├── README.md
├── dfx.json
└── src
├── Bucket-HTTP
├── Bucket-HTTP.mo
└── example.mo
└── Bucket
├── Bucket.mo
└── example.mo
/README.md:
--------------------------------------------------------------------------------
1 | # Bucket
2 |
3 | The library is a **storage library** for canisters to manage Stable Memory.
4 |
5 | As far as we know, canisters that storage data into stable memory have many advantages, such as :
6 | - upgradable (when rts memory goes large)
7 | - larger storage space : Stable Memory can be allocated to 8 GB as present
8 | - no GC cost
9 |
10 | Therefore, In order to be compatible with the existing development ecology, we develop two versions :
11 |
12 | - [Bucket](#Bucket)
13 | - [Bucket-HTTP](#Bucket-HTTP)
14 |
15 | You can use this as simple as using the TireMap.
16 |
17 |
18 |
19 | ## Bucket
20 |
21 |
22 |
23 | - First, you need to import Bucket in your project
24 |
25 | ```motoko
26 | import Bucket "Bucket";
27 | ```
28 |
29 | - Second, you need to declare a Bucket
30 |
31 | **upgrade** :This means that you can upgrade your canister without discarding files stored in the stablememory. Meanwhile available stablememory will become **aval_stablememory** = **8G - heap memory**
32 |
33 | ```motoko
34 | let bucket = Bucket.Bucket(true); // true : upgradable, false : unupgradable
35 | ```
36 |
37 | You have a few more things to do below:
38 |
39 | 1. you should use a stable entries to store your key-value pairs during upgrade
40 |
41 | ```motoko
42 | stable var bucket_entries: [(Text,[(Nat64, Nat)])] = [];
43 | ```
44 |
45 | 2. You also need to configure the system function [preupgrade and postupgrade](https://smartcontracts.org/docs/language-guide/upgrades.html#_preupgrade_and_postupgrade_system_methods)
46 |
47 | ```motoko
48 | system func preupgrade(){
49 | bucket_entries := bucket.preupgrade();
50 | };
51 |
52 | system func postupgrade(){
53 | bucket.postupgrade(bucket_entries);
54 | bucket_entries := [];
55 | };
56 | ```
57 |
58 |
59 | **nonupgradable** : This means that if you upgrade your canister, you will discard files stored in the stablememory
60 | ```motoko
61 | let bucket = Bucket.Bucket(false); // true : upgradable, false : nonupgradable
62 | ```
63 |
64 |
65 | - Third,configure the **dfx.json**
66 |
67 | you should point out how much stable memory pages you want to use in dfx.json(recommendation : 131072)
68 |
69 | ```motoko
70 | "build" :{
71 | "args": "--max-stable-pages=131072" // the max size is 131072 [131072 = 8G / 64KB(each page size)]
72 | }
73 | ```
74 |
75 | **[more details please read the demo](https://github.com/PrimLabs/Bucket/blob/main/src/Bucket/example.mo)**
76 |
77 | ### API
78 |
79 | - **put** :put the value into stablememory,use key to index
80 |
81 | if you add it again with the same key, it will overwrite the previous file
82 |
83 | ```motoko
84 | public func put(key: Text, value : Blob): Result.Result<(), Error>
85 | ```
86 |
87 | tips: you can transform any type T to Text by using ``debug_show(t: T)``
88 |
89 | - **append** :put the value into stablememory,use key to index
90 |
91 | if added again with the same key, it will be merged with the previous file block
92 |
93 | ```motoko
94 | public func append(key: Text, value : Blob): Result.Result<(), Error>
95 | ```
96 |
97 | tips: you can transform any type T to Text by using ``debug_show(t: T)``
98 |
99 | - **get** : use the key to get the value
100 |
101 | ```motoko
102 | public func get(key: Text): Result.Result<[Blob], Error>
103 | ```
104 |
105 | - **preupgrade** : return entries
106 |
107 | ```motoko
108 | public func preupgrade(): [(Text, [(Nat64, Nat)])]
109 | ```
110 |
111 | - **postupgrade**
112 |
113 | ```motoko
114 | public func postupgrade(entries : [(Text, [(Nat64, Nat)])]): ()
115 | ```
116 |
117 |
118 |
119 | ## Bucket-HTTP
120 |
121 | The difference between Bucket-HTTP and Bucket is that Bucket-HTTP has built-in **http_request**, so people can query files through example : **canisterID.raw.ic0.app/static/key**
122 |
123 | example
124 |
125 | ```
126 | https://2fli5-jyaaa-aaaao-aabea-cai.raw.ic0.app/static/0
127 | ```
128 |
129 | Due to the problem of IC mainnet, HTTP-StreamingCallback cannot work at present, so only files less than or equal to **3M** can be accessed through http.
130 |
131 | **We will fix this deficiency as soon as possible.**
132 |
133 | [**The preparation is almost the same as the above, just change Bucket to Bucket-HTTP**](#prework)
134 |
135 | **[more details please read the demo](https://github.com/PrimLabs/Bucket/blob/main/src/Bucket-HTTP/example.mo)**
136 |
137 | ### API
138 |
139 | - **put** :put the value into stablememory,use key to index
140 |
141 | if you add it again with the same key, it will overwrite the previous file
142 |
143 | ```motoko
144 | public func put(key: Text, value : Blob): Result.Result<(), Error>
145 | ```
146 |
147 | tips: you can transform any type T to Text by using ``debug_show(t: T)``
148 |
149 | - **append** :put the value into stablememory,use key to index
150 |
151 | if added again with the same key, it will be merged with the previous file block
152 |
153 | ```motoko
154 | public func append(key: Text, value : Blob): Result.Result<(), Error>
155 | ```
156 |
157 | tips: you can transform any type T to Text by using ``debug_show(t: T)``
158 |
159 | - **get** : use the key to get the value
160 |
161 | ```motoko
162 | public func get(key: Text): Result.Result<[Blob], Error>
163 | ```
164 |
165 | - **build_http** : Pass in the function that parses the key in the url,the key is used to get the value
166 |
167 | ATTENTION : YOU MUST SET YOUR DECODE FUNCITON OR REWRITE IT AND CALL THE BUILD FUNCTION TO ENABLE IT WHEN YOU NEED TO USE THE HTTP INTERFACE.
168 |
169 | ```motoko
170 | public func build_http(fn_: DecodeUrl): ()
171 | ```
172 |
173 | ```motoko
174 | public type DecodeUrl = (Text) -> (Text);
175 | ```
176 |
177 | - **http_request**
178 |
179 | ```motoko
180 | public func http_request(request: HttpRequest): HttpResponse
181 | ```
182 |
183 | - **preupgrade** : return entries
184 |
185 | ```motoko
186 | public func preupgrade(): [(Text, [(Nat64, Nat)])]
187 | ```
188 |
189 | - **postupgrade**
190 |
191 | ```motoko
192 | public func postupgrade(entries : [(Text, [(Nat64, Nat)])]): ()
193 | ```
194 |
195 |
196 | ## Disclaimer
197 |
198 | YOU EXPRESSLY ACKNOWLEDGE AND AGREE THAT USE OF THIS SOFTWARE IS AT YOUR SOLE RISK. AUTHORS OF THIS SOFTWARE SHALL NOT BE LIABLE FOR DAMAGES OF ANY TYPE, WHETHER DIRECT OR INDIRECT.
199 |
200 | ## Contributing
201 |
202 |
203 |
204 | We'd like to collaborate with the community to provide better data storage standard implementation for the developers on the IC, if you have some ideas you'd like to discuss, submit an issue, if you want to improve the code or you made a different implementation, make a pull request!
205 |
--------------------------------------------------------------------------------
/dfx.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "dfx": "0.9.3",
4 | "canisters": {
5 | "example": {
6 | "type": "motoko",
7 | "main": "src/Bucket-HTTP/example.mo"
8 | }
9 | },
10 | "defaults": {
11 | "build": {
12 | "packtool": "",
13 | "args": "--max-stable-pages=131072"
14 | }
15 | },
16 | "networks": {
17 | "local": {
18 | "bind": "127.0.0.1:8000",
19 | "type": "ephemeral"
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Bucket-HTTP/Bucket-HTTP.mo:
--------------------------------------------------------------------------------
1 | import TrieMap "mo:base/TrieMap";
2 | import Result "mo:base/Result";
3 | import Blob "mo:base/Blob";
4 | import Array "mo:base/Array";
5 | import Text "mo:base/Text";
6 | import Nat64 "mo:base/Nat64";
7 | import Nat "mo:base/Nat";
8 | import SM "mo:base/ExperimentalStableMemory";
9 | import Prim "mo:⛔";
10 |
11 | module {
12 |
13 | public type Error = {
14 | #INSUFFICIENT_MEMORY;
15 | #BlobSizeError;
16 | #INVALID_KEY;
17 | #Append_Error;
18 | };
19 |
20 | public type HeaderField = (Text, Text);
21 | public type StreamingCallbackResponse = {
22 | body: Blob;
23 | token: ?CallbackToken;
24 | };
25 | public type CallbackToken = {
26 | index: Nat;
27 | key: Text;
28 | };
29 | public type StreamingCallback = query (CallbackToken) -> async (StreamingCallbackResponse);
30 | public type StreamingStrategy = {
31 | #Callback: {
32 | callback: StreamingCallback;
33 | token: CallbackToken;
34 | }
35 | };
36 | public type HttpRequest = {
37 | method: Text;
38 | url: Text;
39 | headers: [HeaderField];
40 | body: Blob;
41 | };
42 | public type HttpResponse = {
43 | status_code: Nat16;
44 | headers: [HeaderField];
45 | body: Blob;
46 | streaming_strategy: ?StreamingStrategy;
47 | };
48 |
49 | public type DecodeUrl = (Text) -> (Text, Text);
50 |
51 | public class BucketHttp(upgradable : Bool) {
52 | private let THRESHOLD = 6442450944;
53 | // MAX_PAGE_SIZE = 8 GB(total size of stable memory currently) / 64 KB(each page size = 64 KB)
54 | private let MAX_PAGE_BYTE = 65536;
55 | private let MAX_PAGE_NUMBER = 131072 : Nat64;
56 | private let MAX_QUERY_SIZE = 3144728;
57 | private var offset = 8; // 0 - 7 is used for offset
58 | private var decodeurl: ?DecodeUrl = null;
59 | var assets = TrieMap.TrieMap(Text.equal, Text.hash);
60 |
61 | public func put(key: Text, value : Blob): Result.Result<(), Error> {
62 | switch(_getField(value.size())) {
63 | case(#ok(field)) {
64 | assets.put(key, [field]);
65 | _storageData(field.0, value);
66 | };
67 | case(#err(err)) { return #err(err) };
68 | };
69 | #ok(())
70 | };
71 |
72 | public func append(key: Text, value : Blob): Result.Result<(), Error> {
73 | switch(_getField(value.size())) {
74 | case(#ok(field)) {
75 | switch(assets.get(key)){
76 | case null { return #err(#Append_Error);};
77 | case(?pre_field){
78 | let present_field = Array.append<(Nat64, Nat)>(pre_field, [field]);
79 | assets.put(key, present_field);
80 | };
81 | };
82 | _storageData(field.0, value);
83 | };
84 | case(#err(err)) { return #err(err) };
85 | };
86 | #ok(())
87 | };
88 |
89 | public func get(key: Text): Result.Result<[Blob], Error> {
90 | switch(assets.get(key)) {
91 | case(null) { return #err(#INVALID_KEY) };
92 | case(?field) {
93 | let res = Array.init(field.size(), "":Blob);
94 | var index = 0;
95 | for(f in field.vals()){
96 | res[index] := _loadFromSM(f);
97 | index += 1;
98 | };
99 | #ok(Array.freeze(res))
100 | };
101 | };
102 | };
103 |
104 | public func http_request(request: HttpRequest,callbackfunc: StreamingCallback): HttpResponse {
105 | switch(decodeurl) {
106 | case(null) { return errStaticpage("Decodeurl Funtion Wrong");};
107 | case(?_decodeurl) {
108 | let info = _decodeurl(request.url);
109 | let fileKey = info.1;let fileType = info.0;
110 | switch(get(fileKey)) {
111 | case(#err(err)) { return errStaticpage("get wrong");};
112 | case(#ok(payload)) {
113 | let number = payload.size();
114 | if(number == 1) {
115 | return {
116 | status_code = 200;
117 | headers = getContentType(fileType);
118 | body = payload[0];
119 | streaming_strategy = null;
120 | };
121 | } else {
122 | return {
123 | status_code = 200;
124 | headers = getContentType(fileType);
125 | body = payload[0];
126 | streaming_strategy = ?#Callback({
127 | callback = callbackfunc;
128 | token = {
129 | index = 1;
130 | key = fileKey;
131 | };
132 | });
133 | }
134 | }
135 | };
136 | };
137 | };
138 | };
139 | errStaticpage("Somting Wrong")
140 | };
141 |
142 | public func streamingCallback(token: CallbackToken): StreamingCallbackResponse {
143 | var payload: [Blob] = [];
144 | switch(get(token.key)) {
145 | case(#err(err)) {};
146 | case(#ok(ans)) { payload := ans;};
147 | };
148 | {
149 | body = payload[token.index];
150 | token = if(token.index + 1 == payload.size() ) {
151 | null
152 | } else {
153 | ?{
154 | index = token.index + 1;
155 | key = token.key;
156 | }
157 | };
158 | }
159 | };
160 |
161 | public func build_http(fn_: DecodeUrl): () {
162 | decodeurl := ?fn_;
163 | };
164 |
165 | // return entries
166 | public func preupgrade(): [(Text, [(Nat64, Nat)])] {
167 | SM.storeNat64(0 : Nat64, Nat64.fromNat(offset));
168 | var index = 0;
169 | var assets_entries = Array.init<(Text, [(Nat64, Nat)])>(assets.size(), ("", []));
170 | for (kv in assets.entries()) {
171 | assets_entries[index] := kv;
172 | index += 1;
173 | };
174 | Array.freeze<(Text, [(Nat64, Nat)])>(assets_entries)
175 | };
176 |
177 | public func postupgrade(entries : [(Text, [(Nat64, Nat)])]): () {
178 | offset := Nat64.toNat(SM.loadNat64(0:Nat64));
179 | assets := TrieMap.fromEntries(entries.vals(), Text.equal, Text.hash);
180 | };
181 |
182 | private func getContentType(fileType: Text): [HeaderField] {
183 | if(fileType == "gif") return [("Content-Type", "image/gif")];
184 | if(fileType == "jpeg") return [("Content-Type", "image/jpeg")];
185 | if(fileType == "png") return [("Content-Type", "image/png")];
186 | if(fileType == "pdf") return [("Content-Type", "application/pdf")];
187 | if(fileType == "doc") return [("Content-Type", "application/msword")];
188 | if(fileType == "mp3") return [("Content-Type", "audio/mp3")];
189 | if(fileType == "mp4") return [("Content-Type", "video/mp4")];
190 | if(fileType == "txt") return [("Content-Type", "text/plain")];
191 | if(fileType == "ppt") return [("Content-Type", "application/vnd.ms-powerpoint")];
192 | if(fileType == "css") return [("Content-Type", "text/css")];
193 | return [("Content-Type", "text/html; charset=utf-8")];
194 | };
195 |
196 | private func _loadFromSM(field : (Nat64, Nat)) : Blob {
197 | SM.loadBlob(field.0, field.1)
198 | };
199 |
200 | private func _getField(total_size : Nat) : Result.Result<(Nat64, Nat), Error> {
201 | switch (_inspectSize(total_size)) {
202 | case (#err(err)) { #err(err) };
203 | case (#ok(_)) {
204 | let field = (Nat64.fromNat(offset), total_size);
205 | _growStableMemoryPage(total_size);
206 | offset += total_size;
207 | #ok(field)
208 | };
209 | }
210 | };
211 |
212 | // check total_size
213 | private func _inspectSize(total_size : Nat) : Result.Result<(), Error> {
214 | if (total_size <= _getAvailableMemorySize()) { #ok(()) } else { #err(#INSUFFICIENT_MEMORY) };
215 | };
216 |
217 | // upload时根据分配好的write_page以vals的形式写入数据
218 | // When uploading, write data in the form of vals according to the assigned write_page
219 | private func _storageData(start : Nat64, data : Blob) {
220 | SM.storeBlob(start, data)
221 | };
222 |
223 | // return available memory size can be allocated
224 | private func _getAvailableMemorySize() : Nat{
225 | if(upgradable){
226 | assert(THRESHOLD >= Prim.rts_memory_size() + offset);
227 | THRESHOLD - Prim.rts_memory_size() - offset
228 | }else{
229 | THRESHOLD - offset
230 | }
231 | };
232 |
233 | // grow SM memory pages of size "size"
234 | private func _growStableMemoryPage(size : Nat) {
235 | if(offset == 8){ ignore SM.grow(1 : Nat64) };
236 | let available_mem : Nat = Nat64.toNat(SM.size()) * MAX_PAGE_BYTE + 1 - offset;
237 | if (available_mem < size) {
238 | let need_allo_size : Nat = size - available_mem;
239 | let growPage = Nat64.fromNat(need_allo_size / MAX_PAGE_BYTE + 1);
240 | ignore SM.grow(growPage);
241 | }
242 | };
243 |
244 | private func errStaticpage(err: Text): HttpResponse {
245 | {
246 | status_code = 404;
247 | headers = [("Content-Type", "text/plain")];
248 | body = Text.encodeUtf8(err);
249 | streaming_strategy = null;
250 | }
251 | };
252 |
253 | };
254 | };
255 |
--------------------------------------------------------------------------------
/src/Bucket-HTTP/example.mo:
--------------------------------------------------------------------------------
1 | import BucketHttp "Bucket-HTTP";
2 | import Blob "mo:base/Blob";
3 | import Text "mo:base/Text";
4 | import Array "mo:base/Array";
5 | import Result "mo:base/Result";
6 | import Iter "mo:base/Iter";
7 | import Nat "mo:base/Nat";
8 | import Debug "mo:base/Debug";
9 | import SM "mo:base/ExperimentalStableMemory";
10 |
11 | actor example{
12 | type HttpRequest = BucketHttp.HttpRequest;
13 | type HttpResponse = BucketHttp.HttpResponse;
14 | type CallbackToken = BucketHttp.CallbackToken;
15 | type StreamingCallbackResponse = BucketHttp.StreamingCallbackResponse;
16 | type Error = BucketHttp.Error;
17 |
18 | stable var entries : [(Text, [(Nat64, Nat)])] = [];
19 | let bucket = BucketHttp.BucketHttp(true); // true : upgradable, false : unupgradable
20 |
21 | // CanisterId.raw.ic0.app/fileType/fileKey
22 | private func decodeurl(url: Text): (Text, Text) { //(fileType, fileKey)
23 | let path = Iter.toArray(Text.tokens(url, #text("/")));
24 | if(path.size() == 2) return (path[0], path[1]);
25 | return ("txt", "Wrong key");
26 | };
27 |
28 | public shared func build_http(): async () {
29 | bucket.build_http(decodeurl);
30 | };
31 |
32 | public query func streamingCallback(token: CallbackToken): async StreamingCallbackResponse {
33 | bucket.streamingCallback(token)
34 | };
35 |
36 | public query func http_request(request: HttpRequest): async HttpResponse {
37 | bucket.http_request(request, streamingCallback)
38 | };
39 |
40 | public query func getBlob(key: Text) : async Result.Result<[Blob], Error>{
41 | switch(bucket.get(key)){
42 | case(#err(e)){ #err(e) };
43 | case(#ok(blob)){
44 | #ok(blob)
45 | }
46 | }
47 | };
48 |
49 | public shared func putImg(key: Text,value: Blob,index: Nat) : async Result.Result<(), Error>{
50 | if(index == 1) {
51 | switch(bucket.put(key, value)){
52 | case(#err(e)){ return #err(e) };
53 | case(_){ return #ok(());};
54 | };
55 | };
56 | if(index > 1) {
57 | switch(bucket.append(key, value)){
58 | case(#err(e)){ return #err(e) };
59 | case(_){ return #ok(());};
60 | };
61 | };
62 | #ok(())
63 | };
64 |
65 | public shared func putBlob() : async Result.Result<(), Error>{
66 | let key = "key";
67 | let value = Text.encodeUtf8("this is the value");
68 | switch(bucket.put(key, value)){
69 | case(#err(e)){ return #err(e) };
70 | case(_){};
71 | };
72 | #ok(())
73 | };
74 |
75 | system func preupgrade(){
76 | entries := bucket.preupgrade();
77 | };
78 |
79 | system func postupgrade(){
80 | bucket.postupgrade(entries);
81 | entries := [];
82 | };
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/src/Bucket/Bucket.mo:
--------------------------------------------------------------------------------
1 | import TrieMap "mo:base/TrieMap";
2 | import Result "mo:base/Result";
3 | import Blob "mo:base/Blob";
4 | import Array "mo:base/Array";
5 | import Text "mo:base/Text";
6 | import Nat64 "mo:base/Nat64";
7 | import SM "mo:base/ExperimentalStableMemory";
8 | import Prim "mo:⛔";
9 |
10 | module {
11 |
12 | public type Error = {
13 | #INSUFFICIENT_MEMORY;
14 | #BlobSizeError;
15 | #INVALID_KEY;
16 | #Append_Error;
17 | };
18 |
19 | public class Bucket(upgradable : Bool) {
20 | private let THRESHOLD = 6442450944;
21 | // MAX_PAGE_SIZE = 8 GB(total size of stable memory currently) / 64 KB(each page size = 64 KB)
22 | private let MAX_PAGE_BYTE = 65536;
23 | private let MAX_PAGE_NUMBER = 131072 : Nat64;
24 | private let MAX_QUERY_SIZE = 3144728;
25 | private var offset = 8; // 0 - 7 is used for offset
26 | var assets = TrieMap.TrieMap(Text.equal, Text.hash);
27 |
28 | public func put(key: Text, value : Blob): Result.Result<(), Error> {
29 | switch(_getField(value.size())) {
30 | case(#ok(field)) {
31 | assets.put(key, [field]);
32 | _storageData(field.0, value);
33 | };
34 | case(#err(err)) { return #err(err) };
35 | };
36 | #ok(())
37 | };
38 |
39 | public func append(key: Text, value : Blob): Result.Result<(), Error> {
40 | switch(_getField(value.size())) {
41 | case(#ok(field)) {
42 | switch(assets.get(key)){
43 | case null { return #err(#Append_Error);};
44 | case(?pre_field){
45 | let present_field = Array.append<(Nat64, Nat)>(pre_field, [field]);
46 | assets.put(key, present_field);
47 | };
48 | };
49 | _storageData(field.0, value);
50 | };
51 | case(#err(err)) { return #err(err) };
52 | };
53 | #ok(())
54 | };
55 |
56 | public func get(key: Text): Result.Result<[Blob], Error> {
57 | switch(assets.get(key)) {
58 | case(null) { return #err(#INVALID_KEY) };
59 | case(?field) {
60 | let res = Array.init(field.size(), "":Blob);
61 | var index = 0;
62 | for(f in field.vals()){
63 | res[index] := _loadFromSM(f);
64 | index += 1;
65 | };
66 | #ok(Array.freeze(res))
67 | };
68 | };
69 | };
70 |
71 | // return entries
72 | public func preupgrade(): [(Text, [(Nat64, Nat)])] {
73 | SM.storeNat64(0 : Nat64, Nat64.fromNat(offset));
74 | var index = 0;
75 | var assets_entries = Array.init<(Text, [(Nat64, Nat)])>(assets.size(), ("", []));
76 | for (kv in assets.entries()) {
77 | assets_entries[index] := kv;
78 | index += 1;
79 | };
80 | Array.freeze<(Text, [(Nat64, Nat)])>(assets_entries)
81 | };
82 |
83 | public func postupgrade(entries : [(Text, [(Nat64, Nat)])]): () {
84 | offset := Nat64.toNat(SM.loadNat64(0:Nat64));
85 | assets := TrieMap.fromEntries(entries.vals(), Text.equal, Text.hash);
86 | };
87 |
88 | private func _loadFromSM(field : (Nat64, Nat)) : Blob {
89 | SM.loadBlob(field.0, field.1)
90 | };
91 |
92 | private func _getField(total_size : Nat) : Result.Result<(Nat64, Nat), Error> {
93 | switch (_inspectSize(total_size)) {
94 | case (#err(err)) { #err(err) };
95 | case (#ok(_)) {
96 | let field = (Nat64.fromNat(offset), total_size);
97 | _growStableMemoryPage(total_size);
98 | offset += total_size;
99 | #ok(field)
100 | };
101 | }
102 | };
103 |
104 | // check total_size
105 | private func _inspectSize(total_size : Nat) : Result.Result<(), Error> {
106 | if (total_size <= _getAvailableMemorySize()) { #ok(()) } else { #err(#INSUFFICIENT_MEMORY) };
107 | };
108 |
109 | // upload时根据分配好的write_page以vals的形式写入数据
110 | // When uploading, write data in the form of vals according to the assigned write_page
111 | private func _storageData(start : Nat64, data : Blob) {
112 | SM.storeBlob(start, data)
113 | };
114 |
115 | // return available memory size can be allocated
116 | private func _getAvailableMemorySize() : Nat{
117 | if(upgradable){
118 | assert(THRESHOLD >= Prim.rts_memory_size() + offset);
119 | THRESHOLD - Prim.rts_memory_size() - offset
120 | }else{
121 | THRESHOLD - offset
122 | }
123 | };
124 |
125 | // grow SM memory pages of size "size"
126 | private func _growStableMemoryPage(size : Nat) {
127 | if(offset == 8){ ignore SM.grow(1 : Nat64) };
128 | let available_mem : Nat = Nat64.toNat(SM.size()) * MAX_PAGE_BYTE + 1 - offset;
129 | if (available_mem < size) {
130 | let need_allo_size : Nat = size - available_mem;
131 | let growPage = Nat64.fromNat(need_allo_size / MAX_PAGE_BYTE + 1);
132 | ignore SM.grow(growPage);
133 | }
134 | };
135 |
136 | };
137 | };
138 |
--------------------------------------------------------------------------------
/src/Bucket/example.mo:
--------------------------------------------------------------------------------
1 | import Bucket "Bucket";
2 | import Blob "mo:base/Blob";
3 | import Text "mo:base/Text";
4 | import Array "mo:base/Array";
5 | import Result "mo:base/Result";
6 | import Nat "mo:base/Nat";
7 | import Debug "mo:base/Debug";
8 |
9 | actor example{
10 |
11 | type Error = Bucket.Error;
12 | type S = {
13 | text : Text;
14 | bool : Bool
15 | };
16 | stable var bucket_entries : [(Text, [(Nat64, Nat)])] = [];
17 | let bucket = Bucket.Bucket(true); // true : upgradable, false : unupgradable
18 |
19 | public query func getBlob(key : Text) : async Result.Result<[Blob], Error>{
20 | switch(bucket.get(key)){
21 | case(#err(e)){ #err(e) };
22 | case(#ok(blob)){
23 | #ok(blob)
24 | }
25 | }
26 | };
27 |
28 | public query func get(key : Text) : async Result.Result<[S], Error>{
29 | switch(bucket.get(key)){
30 | case(#err(info)){ #err(info) };
31 | case(#ok(data)){ #ok(deserialize(data)) };
32 | };
33 | };
34 |
35 | public func put() : async Result.Result<(), Error>{
36 | let key = "key";
37 | let value_1 : S = {
38 | text = "this is the first slice of value";
39 | bool = true
40 | };
41 | let value_2 : S = {
42 | text = "this is the second slice of value";
43 | bool = false
44 | };
45 | switch(bucket.put(key, serialize(value_1))){
46 | case(#err(e)){ return #err(e) };
47 | case(_){};
48 | };
49 | // you can storage the two different value using the same key
50 | switch(bucket.append(key, serialize(value_2))){
51 | case(#err(e)){ return #err(e) };
52 | case(_){};
53 | };
54 | #ok(())
55 | };
56 |
57 | public func putBlob() : async Result.Result<(), Error>{
58 | let key = "key";
59 | let value = Text.encodeUtf8("this is the value");
60 | switch(bucket.put(key, value)){
61 | case(#err(e)){ return #err(e) };
62 | case(_){};
63 | };
64 | #ok(())
65 | };
66 |
67 | system func preupgrade(){
68 | bucket_entries := bucket.preupgrade();
69 | };
70 |
71 | system func postupgrade(){
72 | bucket.postupgrade(bucket_entries);
73 | bucket_entries := [];
74 | };
75 |
76 | // you should encode the segment of the struct into nat8
77 | // then you should merge them and transform the [Nat8] to Blob
78 | private func serialize(s : S) : Blob{
79 | let bool_nat8 = if(s.bool){
80 | 1 : Nat8
81 | }else{ 0 : Nat8 };
82 | let text_blob = Text.encodeUtf8(s.text);
83 | let text_nat8 = Blob.toArray(text_blob);
84 | let serialize_data = Array.append(text_nat8, [bool_nat8]);
85 | Blob.fromArray(serialize_data)
86 | };
87 |
88 | private func deserialize(data : [Blob]) : [S] {
89 | let res = Array.init(data.size(), {
90 | text = "";
91 | bool = true;
92 | });
93 | var res_index = 0;
94 | for(d in data.vals()){
95 | let raw = Blob.toArray(d);
96 | let bool = if(raw[Nat.sub(raw.size(), 1)] == 1){ true }else{ false };
97 | let text = Array.init(Nat.sub(data.size(), 2), 0:Nat8);// the last byte is used to store the "bool"
98 | var index = 0;
99 | label l for(d in raw.vals()){
100 | text[index] := d;
101 | index += 1;
102 | if(index == text.size()){ break l };
103 | };
104 | let t =
105 | switch(Text.decodeUtf8(Blob.fromArray(Array.freeze(text)))){
106 | case null { "" };
107 | case(?te){ te };
108 | };
109 | res[res_index] :=
110 | {
111 | text = t;
112 | bool = bool
113 | };
114 | res_index += 1;
115 | };
116 | Array.freeze(res)
117 | };
118 |
119 | }
120 |
--------------------------------------------------------------------------------