@filefreak.io/sdk 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,318 @@
1
+ // The developer API's endpoints, and the shapes they take and answer with,
2
+ // written out from the table in api/src/routes/v1.ts by
3
+ // api/src/scripts/clientTypes.ts. Change the table, not this, and run
4
+ // `npm run clients:types`: tests/checks/clients-are-current.mjs fails while
5
+ // this is behind it.
6
+ /** Every endpoint, by the name its entry gives it. */
7
+ export const OPERATIONS = {
8
+ "files.list": { method: "GET", path: "/v1/files", idempotent: false, repeatable: false },
9
+ "files.byPath": { method: "GET", path: "/v1/files/by-path", idempotent: false, repeatable: false },
10
+ "files.get": { method: "GET", path: "/v1/files/:id", idempotent: false, repeatable: false },
11
+ "files.update": { method: "PATCH", path: "/v1/files/:id", idempotent: false, repeatable: false },
12
+ "files.delete": { method: "DELETE", path: "/v1/files/:id", idempotent: false, repeatable: false },
13
+ "files.restore": { method: "POST", path: "/v1/files/:id/restore", idempotent: false, repeatable: false },
14
+ "folders.create": { method: "POST", path: "/v1/folders", idempotent: true, repeatable: false },
15
+ "tree.list": { method: "GET", path: "/v1/tree", idempotent: false, repeatable: false },
16
+ "changes.list": { method: "GET", path: "/v1/changes", idempotent: false, repeatable: false },
17
+ "uploads.begin": { method: "POST", path: "/v1/uploads", idempotent: true, repeatable: false },
18
+ "uploads.get": { method: "GET", path: "/v1/uploads/:id", idempotent: false, repeatable: false },
19
+ "uploads.parts": { method: "GET", path: "/v1/uploads/:id/parts", idempotent: false, repeatable: false },
20
+ "uploads.complete": { method: "POST", path: "/v1/uploads/:id/complete", idempotent: false, repeatable: true },
21
+ "uploads.cancel": { method: "DELETE", path: "/v1/uploads/:id", idempotent: false, repeatable: false },
22
+ "files.downloadUrl": { method: "GET", path: "/v1/files/:id/download", idempotent: false, repeatable: false },
23
+ "files.image": { method: "GET", path: "/v1/files/:id/image", idempotent: false, repeatable: false },
24
+ "usage.get": { method: "GET", path: "/v1/usage", idempotent: false, repeatable: false },
25
+ "me.get": { method: "GET", path: "/v1/me", idempotent: false, repeatable: false },
26
+ "shares.create": { method: "POST", path: "/v1/shares", idempotent: true, repeatable: false },
27
+ "shares.list": { method: "GET", path: "/v1/shares", idempotent: false, repeatable: false },
28
+ "shares.revoke": { method: "DELETE", path: "/v1/shares/:id", idempotent: false, repeatable: false },
29
+ "sites.list": { method: "GET", path: "/v1/sites", idempotent: false, repeatable: false },
30
+ "sites.create": { method: "POST", path: "/v1/sites", idempotent: true, repeatable: false },
31
+ "sites.get": { method: "GET", path: "/v1/sites/:id", idempotent: false, repeatable: false },
32
+ "sites.startDeploy": { method: "POST", path: "/v1/sites/:id/deploys", idempotent: true, repeatable: false },
33
+ "sites.declareFiles": { method: "POST", path: "/v1/sites/deploys/:id/files", idempotent: true, repeatable: false },
34
+ "sites.sendAgain": { method: "POST", path: "/v1/sites/deploys/:id/send", idempotent: false, repeatable: true },
35
+ "sites.finishDeploy": { method: "POST", path: "/v1/sites/deploys/:id/finish", idempotent: false, repeatable: true },
36
+ "sites.removeDeploy": { method: "DELETE", path: "/v1/sites/deploys/:id", idempotent: false, repeatable: false },
37
+ "sites.setLive": { method: "POST", path: "/v1/sites/:id/live", idempotent: false, repeatable: true },
38
+ "tokens.create": { method: "POST", path: "/v1/tokens", idempotent: false, repeatable: true },
39
+ };
40
+ /** Every endpoint as a function, grouped by what it is about. */
41
+ export function bind(call) {
42
+ return {
43
+ files: {
44
+ /**
45
+ * List a folder.
46
+ *
47
+ * What is in a folder, a page at a time: folders first, then files, each by name. Pass nextCursor back as cursor for the next page; when it is null there is no more. Things in the trash are not listed. A page never repeats or skips something because of what was added or removed between two requests.
48
+ *
49
+ * GET /v1/files. Needs the scope files:read.
50
+ */
51
+ list: (query, options) => call("files.list", { query }, options),
52
+ /**
53
+ * A file or folder by its path.
54
+ *
55
+ * Found by the names of the folders down to it from the top of the app (or of the folder the key is confined to), separated by /. A name matches whatever its case, as names in one folder do. Nothing in the trash is found, nor anything inside something that is.
56
+ *
57
+ * GET /v1/files/by-path. Needs the scope files:read.
58
+ */
59
+ byPath: (query, options) => call("files.byPath", { query }, options),
60
+ /**
61
+ * One file or folder.
62
+ *
63
+ * By its id. Something in the trash is still returned, with trashedAt set.
64
+ *
65
+ * GET /v1/files/:id. Needs the scope files:read.
66
+ */
67
+ get: (id, options) => call("files.get", { params: { id } }, options),
68
+ /**
69
+ * Rename, move, or make public.
70
+ *
71
+ * Any of the three, in one request. They are applied in that order, each as its own change: if the second is refused the first has still happened, and the answer says which was refused. Moving a folder moves everything in it and is one change however much that is. Making a folder public makes everything in it public, except what has a setting of its own.
72
+ *
73
+ * PATCH /v1/files/:id. Needs the scope files:write.
74
+ */
75
+ update: (id, body, options) => call("files.update", { params: { id }, body }, options),
76
+ /**
77
+ * Put in the trash, or delete for good.
78
+ *
79
+ * Without permanent, the file or folder goes to the trash: it can be restored, it still counts toward storage, and it is removed for good after thirty days. With permanent=true it is removed now, with everything inside it, and cannot be brought back.
80
+ *
81
+ * DELETE /v1/files/:id. Needs the scope files:delete.
82
+ */
83
+ delete: (id, query, options) => call("files.delete", { params: { id }, query }, options),
84
+ /**
85
+ * Take out of the trash.
86
+ *
87
+ * It goes back where it was. If that folder has gone it goes to the top, and if its name has been taken in the meantime it comes back with a number after it.
88
+ *
89
+ * POST /v1/files/:id/restore. Needs the scope files:write.
90
+ */
91
+ restore: (id, options) => call("files.restore", { params: { id } }, options),
92
+ /**
93
+ * An address to download a file from.
94
+ *
95
+ * A short-lived address on storage itself. Anybody holding it can fetch the file until it expires, with no key, so it can be handed to a browser. Each one issued counts the file's size toward the month's downloads. For an address that does not expire, make the file public and use its publicUrl. A file under Shard Protection is answered with 409 shard_protected: the developer API cannot read protected files yet.
96
+ *
97
+ * GET /v1/files/:id/download. Needs the scope files:read.
98
+ */
99
+ downloadUrl: (id, query, options) => call("files.downloadUrl", { params: { id }, query }, options),
100
+ /**
101
+ * An image at a smaller size.
102
+ *
103
+ * A copy of an image no more than size pixels on its longest side, as WebP, with the camera's metadata removed. The sizes are 64, 128, 256, 512, 1024 and no others. Each is made once, the first time it is asked for, and kept: until it has been made the answer is 202. Name the sizes when the image is uploaded and they are ready by the time they are wanted. For a public file, add ?size= to its publicUrl. A file under Shard Protection has no smaller copies, and is answered with 409 shard_protected.
104
+ *
105
+ * GET /v1/files/:id/image. Needs the scope files:read.
106
+ */
107
+ image: (id, query, options) => call("files.image", { params: { id }, query }, options),
108
+ },
109
+ folders: {
110
+ /**
111
+ * Make a folder.
112
+ *
113
+ * Folders can be nested up to 32 deep. With ifExists=use, a folder of that name that is already there is returned and nothing is made, which is what a program that makes its folders on every start wants.
114
+ *
115
+ * POST /v1/folders. Needs the scope files:write. Honours an Idempotency-Key.
116
+ */
117
+ create: (body, options) => call("folders.create", { body }, options),
118
+ },
119
+ tree: {
120
+ /**
121
+ * Everything under a folder.
122
+ *
123
+ * Every file and folder under a folder, at every level, a page at a time, each folder before what is inside it. Pass nextCursor back as cursor for the next page; when it is null there is no more. Things in the trash are not listed. Every page carries changesCursor: the place in GET /v1/changes from before the first page was read. Something that changes while the pages are being read may be listed or not, and is in the changes after that place either way. It is null for a key confined to a folder, which cannot follow the changes.
124
+ *
125
+ * GET /v1/tree. Needs the scope files:read.
126
+ */
127
+ list: (query, options) => call("tree.list", { query }, options),
128
+ },
129
+ changes: {
130
+ /**
131
+ * What has changed.
132
+ *
133
+ * Every change to the app's files and folders after cursor, oldest first, each saying what the thing is now. A folder's change stands for everything in it: a folder moved, put in the trash or deleted is one change, and what is inside goes with it. Without cursor, no changes and the cursor for now. Pass cursor back each time; while more is true, ask again at once. With wait, an answer that would be empty waits up to that many seconds for something to change, and comes back empty, with the same cursor, if nothing does.
134
+ *
135
+ * To keep a copy: list everything with GET /v1/tree and keep its changesCursor, then apply each change in order. A change that is not live, or whose parentId is a folder the copy does not have, takes that thing and everything inside it out of the copy. A live change puts it in, or moves or renames it; a folder that was not in the copy is then listed with GET /v1/tree?parent= for what it holds, and if that is answered 404 the folder has gone since, which a later change says. A file whose revision has gone up has new content. Changes are kept for 30 days, and a cursor from before that is answered 410 cursor_expired: start again from a listing. A key confined to a folder cannot follow the changes, and lists its folder again instead.
136
+ *
137
+ * While the service is asleep, after a while with nobody using it, nothing can change, and a question with a cursor is answered without waking it: no changes, the same cursor, after the wait asked for. Asking is not counted as using the service, so a program that asks all night does not keep it awake.
138
+ *
139
+ * GET /v1/changes. Needs the scope files:read.
140
+ */
141
+ list: (query, options) => call("changes.list", { query }, options),
142
+ },
143
+ uploads: {
144
+ /**
145
+ * Begin an upload.
146
+ *
147
+ * Says what is about to be sent, and answers with where to send it. A file's bytes never pass through this API: they go straight to storage, in parts, each with a plain PUT to the address given for it. The first addresses come back here; ask for the rest as they are needed. Then complete the upload, and it becomes a file.
148
+ *
149
+ * The room is set aside now, so an upload that would not fit is refused before a byte is sent. The parts can be sent by anything that can make a PUT request, including a browser that has never seen the API key: hand it the addresses.
150
+ *
151
+ * POST /v1/uploads. Needs the scope files:write. Honours an Idempotency-Key.
152
+ */
153
+ begin: (body, options) => call("uploads.begin", { body }, options),
154
+ /**
155
+ * An upload, and which parts have arrived.
156
+ *
157
+ * For carrying on after an interruption: storage itself says which parts it has, so nothing the client remembered is needed.
158
+ *
159
+ * GET /v1/uploads/:id. Needs the scope files:write.
160
+ */
161
+ get: (id, options) => call("uploads.get", { params: { id } }, options),
162
+ /**
163
+ * Addresses for more parts.
164
+ *
165
+ * Addresses are given out a window at a time and each lasts half an hour. Ask again for any that have run out.
166
+ *
167
+ * GET /v1/uploads/:id/parts. Needs the scope files:write.
168
+ */
169
+ parts: (id, query, options) => call("uploads.parts", { params: { id }, query }, options),
170
+ /**
171
+ * Finish an upload.
172
+ *
173
+ * Turns the parts into a file. Safe to call again: a second call for an upload that has finished answers with the same file. For a very large file storage may still be putting it together, and the answer is then 202 with how long to wait before asking again.
174
+ *
175
+ * POST /v1/uploads/:id/complete. Needs the scope files:write. Safe to send again.
176
+ */
177
+ complete: (id, body, options) => call("uploads.complete", { params: { id }, body }, options),
178
+ /**
179
+ * Cancel an upload.
180
+ *
181
+ * Whatever had arrived is deleted, and the room it held comes back at once.
182
+ *
183
+ * DELETE /v1/uploads/:id. Needs the scope files:write.
184
+ */
185
+ cancel: (id, options) => call("uploads.cancel", { params: { id } }, options),
186
+ },
187
+ usage: {
188
+ /**
189
+ * What the app is using.
190
+ *
191
+ * What this app holds, against the ceiling set for it if there is one; and what the account it belongs to has used this calendar month, across its personal files and all its apps, against what its plan includes. An app's files count toward the account's one storage allowance. For a device signed in with ff login, which uses the personal drive, app is null.
192
+ *
193
+ * GET /v1/usage. Needs the scope usage:read.
194
+ */
195
+ get: (options) => call("usage.get", {}, options),
196
+ },
197
+ me: {
198
+ /**
199
+ * Who this is.
200
+ *
201
+ * The account a key, a token or a device acts for, and what it may do there. A key's program and a page's token are told the account's username and plan, never its email address: only a device signed in with ff login, which is the person, is told that.
202
+ *
203
+ * GET /v1/me. Any key may call it.
204
+ */
205
+ get: (options) => call("me.get", {}, options),
206
+ },
207
+ shares: {
208
+ /**
209
+ * Make a share link.
210
+ *
211
+ * A link that opens one file or folder for whoever holds it, separate from making it public: a private file can have one. It can have a password, an end date and a number of downloads after which it stops. Share links are a person's, so only a device signed in with ff login can make them here, as on the website; an app's key cannot.
212
+ *
213
+ * POST /v1/shares. Only a device signed in with ff login may call it. Honours an Idempotency-Key.
214
+ */
215
+ create: (body, options) => call("shares.create", { body }, options),
216
+ /**
217
+ * List share links.
218
+ *
219
+ * The links on one file or folder, or every link the account has, newest first. Removed links are not listed. Only a device signed in with ff login.
220
+ *
221
+ * GET /v1/shares. Only a device signed in with ff login may call it.
222
+ */
223
+ list: (query, options) => call("shares.list", { query }, options),
224
+ /**
225
+ * Remove a share link.
226
+ *
227
+ * It stops working with the next request, and cannot be brought back. Only a device signed in with ff login.
228
+ *
229
+ * DELETE /v1/shares/:id. Only a device signed in with ff login may call it.
230
+ */
231
+ revoke: (id, options) => call("shares.revoke", { params: { id } }, options),
232
+ },
233
+ sites: {
234
+ /**
235
+ * List sites.
236
+ *
237
+ * The account's static sites, by name, with the deploy each serves. A site is a person's, so only a device signed in with ff login reaches sites, as on the website; an app's key cannot. Sites are part of Pro.
238
+ *
239
+ * GET /v1/sites. Only a device signed in with ff login may call it.
240
+ */
241
+ list: (options) => call("sites.list", {}, options),
242
+ /**
243
+ * Make a site.
244
+ *
245
+ * A site with no deploy yet. Its name is its address: 3 to 40 lower-case letters, digits and single hyphens, and not a name FileFreak keeps, another account's username, or one that looks like another company's (invalid_site_name, site_name_taken). Refused with sites_need_pro on a plan without sites.
246
+ *
247
+ * POST /v1/sites. Only a device signed in with ff login may call it. Honours an Idempotency-Key.
248
+ */
249
+ create: (body, options) => call("sites.create", { body }, options),
250
+ /**
251
+ * A site and its deploys.
252
+ *
253
+ * A site, and its deploys newest first. A site keeps its 10 newest deploys that have ended besides the live one.
254
+ *
255
+ * GET /v1/sites/:id. Only a device signed in with ff login may call it.
256
+ */
257
+ get: (id, options) => call("sites.get", { params: { id } }, options),
258
+ /**
259
+ * Begin a deploy.
260
+ *
261
+ * Begins a deploy with up to 1000 of its files: each one's path, size and SHA-256. Declare the rest with sites.declareFiles, more set to false on the last list. The answer says which files to send: PUT each one's bytes to its address, then call sites.finishDeploy. Bytes the site has stored already are not asked for. A deploy may have 20000 files, 100 MB each and 1 GB together, and what it adds counts as storage from the moment it is declared (quota_exceeded). An unfinished deploy nothing has asked about for an hour is given up on.
262
+ *
263
+ * POST /v1/sites/:id/deploys. Only a device signed in with ff login may call it. Honours an Idempotency-Key.
264
+ */
265
+ startDeploy: (id, body, options) => call("sites.startDeploy", { params: { id }, body }, options),
266
+ /**
267
+ * Declare more of a deploy's files.
268
+ *
269
+ * The next 1000 files of a deploy at most, as sites.startDeploy takes them. A path declared twice is refused (path_declared_twice).
270
+ *
271
+ * POST /v1/sites/deploys/:id/files. Only a device signed in with ff login may call it. Honours an Idempotency-Key.
272
+ */
273
+ declareFiles: (id, body, options) => call("sites.declareFiles", { params: { id }, body }, options),
274
+ /**
275
+ * Fresh addresses for a deploy's files.
276
+ *
277
+ * New addresses for files of an unfinished deploy whose bytes have not arrived, when the first ones ran out or a finish answered deploy_incomplete. Files whose bytes have arrived are left out.
278
+ *
279
+ * POST /v1/sites/deploys/:id/send. Only a device signed in with ff login may call it. Safe to send again.
280
+ */
281
+ sendAgain: (id, body, options) => call("sites.sendAgain", { params: { id }, body }, options),
282
+ /**
283
+ * Finish a deploy.
284
+ *
285
+ * Storage puts each file in place and is asked its size, and the deploy is ready, and live if live is true. Safe to call again: storage may take longer than one request waits, and the answer is then 202 with how long to wait before asking again. A file whose bytes have not arrived, or arrived at another size, is answered with deploy_incomplete and the SHA-256s to send again.
286
+ *
287
+ * POST /v1/sites/deploys/:id/finish. Only a device signed in with ff login may call it. Safe to send again.
288
+ */
289
+ finishDeploy: (id, body, options) => call("sites.finishDeploy", { params: { id }, body }, options),
290
+ /**
291
+ * Cancel or delete a deploy.
292
+ *
293
+ * Cancels a deploy that is uploading, and what it set aside is given back at once. Deletes one that has ended, with whatever bytes no other deploy of the site uses. Never the live one (deploy_live).
294
+ *
295
+ * DELETE /v1/sites/deploys/:id. Only a device signed in with ff login may call it.
296
+ */
297
+ removeDeploy: (id, options) => call("sites.removeDeploy", { params: { id } }, options),
298
+ /**
299
+ * Choose the live deploy.
300
+ *
301
+ * Makes a ready deploy the one visitors are served, which is also how a site is rolled back; null takes the site offline. Visitors see the change within a minute.
302
+ *
303
+ * POST /v1/sites/:id/live. Only a device signed in with ff login may call it. Safe to send again.
304
+ */
305
+ setLive: (id, body, options) => call("sites.setLive", { params: { id }, body }, options),
306
+ },
307
+ tokens: {
308
+ /**
309
+ * A token for a web page.
310
+ *
311
+ * A key belongs on a server, and one sent from a web page is refused, because anybody who opens the page can read it. A page that should reach this API itself, to upload straight from the browser or to list a folder, is given a token instead: the server makes one with its key and hands it to the page, which sends it as it would a key. A token may do what its key may, or less: the scopes asked for, inside the folder asked for, until it expires. It is checked against its key on every request, so revoking the key stops its tokens at once, and its calls count as the key's. Nothing about a token is kept, so making one is safe to send again. A token cannot make another. Once Pro has ended and a key can only read, it can still make tokens, which can only read too.
312
+ *
313
+ * POST /v1/tokens. Any key may call it. Safe to send again.
314
+ */
315
+ create: (body, options) => call("tokens.create", { body }, options),
316
+ },
317
+ };
318
+ }
@@ -0,0 +1,6 @@
1
+ export { FileFreak, Walk, type DownloadOptions, type FollowOptions } from "./client.js";
2
+ export * from "./generated.js";
3
+ export { type Sink } from "./ranges.js";
4
+ export { openFragment, sealFragment, SHARD_OVERHEAD, SHARD_VERSION, ShardError, shardKey, ShardLayout, type FragmentAt, type ShardErrorCode, type ShardKey, } from "./shards.js";
5
+ export { FileFreakError, type CallOptions, type ClientOptions, type TokenSource } from "./transport.js";
6
+ export { UploadStopped, type Source, type UploadOptions } from "./uploads.js";
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ // @filefreak.io/sdk: FileFreak's developer API, for Node and the browser.
2
+ //
3
+ // const ff = new FileFreak({ apiKey: process.env.FILEFREAK_KEY });
4
+ // const { items } = await ff.files.list();
5
+ //
6
+ // In a web page, give it a token the page's own server made with its key
7
+ // (ff.tokens.create), never the key. Files on disk are in "@filefreak.io/sdk/node".
8
+ export { FileFreak, Walk } from "./client.js";
9
+ export * from "./generated.js";
10
+ export { openFragment, sealFragment, SHARD_OVERHEAD, SHARD_VERSION, ShardError, shardKey, ShardLayout, } from "./shards.js";
11
+ export { FileFreakError } from "./transport.js";
12
+ export { UploadStopped } from "./uploads.js";
package/dist/md5.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ export declare class Md5 {
2
+ private a;
3
+ private b;
4
+ private c;
5
+ private d;
6
+ /** Bytes waiting for a whole 64-byte block. */
7
+ private readonly tail;
8
+ private tailLength;
9
+ private total;
10
+ private readonly words;
11
+ private done;
12
+ update(data: Uint8Array): this;
13
+ /** The 16-byte digest. The hash cannot be fed after this. */
14
+ digest(): Uint8Array;
15
+ private block;
16
+ }
17
+ /** Bytes as base64, which is how Content-MD5 carries a digest. */
18
+ export declare function base64(bytes: Uint8Array): string;
package/dist/md5.js ADDED
@@ -0,0 +1,130 @@
1
+ // MD5, for the Content-MD5 header on each part of an upload.
2
+ //
3
+ // Storage checks a part against the Content-MD5 it was sent with and refuses
4
+ // it if they differ, so a part damaged on the way, or a file that changed
5
+ // while it was being sent, is never stored as it arrived. WebCrypto offers no
6
+ // MD5, and this package runs in browsers as well as in Node, so it carries
7
+ // its own: the one the website's uploads use (web/lib/md5.ts), checked
8
+ // against the same examples.
9
+ //
10
+ // RFC 1321. Fed in pieces, so a part never has to be held whole to be hashed.
11
+ /** The sine table, written out rather than computed: Math.sin is not required to give the same last bit everywhere. */
12
+ const K = new Int32Array([
13
+ 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
14
+ 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
15
+ 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
16
+ 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
17
+ 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
18
+ 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
19
+ ]);
20
+ /** How far each step rotates, four to a round. */
21
+ const S = [7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21];
22
+ export class Md5 {
23
+ a = 0x67452301;
24
+ b = 0xefcdab89 | 0;
25
+ c = 0x98badcfe | 0;
26
+ d = 0x10325476;
27
+ /** Bytes waiting for a whole 64-byte block. */
28
+ tail = new Uint8Array(64);
29
+ tailLength = 0;
30
+ total = 0;
31
+ words = new Int32Array(16);
32
+ done = false;
33
+ update(data) {
34
+ if (this.done)
35
+ throw new Error("This MD5 has already been finished.");
36
+ this.total += data.length;
37
+ let i = 0;
38
+ if (this.tailLength > 0) {
39
+ const take = Math.min(64 - this.tailLength, data.length);
40
+ this.tail.set(data.subarray(0, take), this.tailLength);
41
+ this.tailLength += take;
42
+ i = take;
43
+ if (this.tailLength < 64)
44
+ return this;
45
+ this.block(this.tail, 0);
46
+ this.tailLength = 0;
47
+ }
48
+ for (; i + 64 <= data.length; i += 64)
49
+ this.block(data, i);
50
+ if (i < data.length) {
51
+ this.tail.set(data.subarray(i));
52
+ this.tailLength = data.length - i;
53
+ }
54
+ return this;
55
+ }
56
+ /** The 16-byte digest. The hash cannot be fed after this. */
57
+ digest() {
58
+ if (this.done)
59
+ throw new Error("This MD5 has already been finished.");
60
+ const bits = this.total * 8;
61
+ const pad = new Uint8Array(this.tailLength < 56 ? 64 - this.tailLength : 128 - this.tailLength);
62
+ pad[0] = 0x80;
63
+ // The length in bits, little-endian, in the last eight bytes. A file over
64
+ // 2^32 bits (512 MiB) needs the high word as well.
65
+ const end = pad.length - 8;
66
+ const low = bits >>> 0;
67
+ const high = Math.floor(bits / 0x100000000) >>> 0;
68
+ for (let k = 0; k < 4; k++) {
69
+ pad[end + k] = (low >>> (8 * k)) & 0xff;
70
+ pad[end + 4 + k] = (high >>> (8 * k)) & 0xff;
71
+ }
72
+ this.total -= pad.length;
73
+ this.update(pad);
74
+ this.done = true;
75
+ const out = new Uint8Array(16);
76
+ for (const [n, word] of [this.a, this.b, this.c, this.d].entries()) {
77
+ for (let k = 0; k < 4; k++)
78
+ out[n * 4 + k] = (word >>> (8 * k)) & 0xff;
79
+ }
80
+ return out;
81
+ }
82
+ block(bytes, at) {
83
+ const w = this.words;
84
+ for (let j = 0; j < 16; j++) {
85
+ const o = at + j * 4;
86
+ w[j] = bytes[o] | (bytes[o + 1] << 8) | (bytes[o + 2] << 16) | (bytes[o + 3] << 24);
87
+ }
88
+ let a = this.a;
89
+ let b = this.b;
90
+ let c = this.c;
91
+ let d = this.d;
92
+ for (let j = 0; j < 64; j++) {
93
+ let f;
94
+ let g;
95
+ if (j < 16) {
96
+ f = (b & c) | (~b & d);
97
+ g = j;
98
+ }
99
+ else if (j < 32) {
100
+ f = (d & b) | (~d & c);
101
+ g = (5 * j + 1) & 15;
102
+ }
103
+ else if (j < 48) {
104
+ f = b ^ c ^ d;
105
+ g = (3 * j + 5) & 15;
106
+ }
107
+ else {
108
+ f = c ^ (b | ~d);
109
+ g = (7 * j) & 15;
110
+ }
111
+ const s = S[(j >> 4) * 4 + (j & 3)];
112
+ const sum = (a + f + K[j] + w[g]) | 0;
113
+ a = d;
114
+ d = c;
115
+ c = b;
116
+ b = (b + ((sum << s) | (sum >>> (32 - s)))) | 0;
117
+ }
118
+ this.a = (this.a + a) | 0;
119
+ this.b = (this.b + b) | 0;
120
+ this.c = (this.c + c) | 0;
121
+ this.d = (this.d + d) | 0;
122
+ }
123
+ }
124
+ /** Bytes as base64, which is how Content-MD5 carries a digest. */
125
+ export function base64(bytes) {
126
+ let text = "";
127
+ for (const byte of bytes)
128
+ text += String.fromCharCode(byte);
129
+ return btoa(text);
130
+ }
package/dist/node.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import type { Sink } from "./ranges.js";
2
+ /**
3
+ * A file on disk, to upload: read a part at a time as it is sent, never held
4
+ * whole. Node refuses to read it if it changes on disk in the meantime, so a
5
+ * file is never sent half old and half new.
6
+ */
7
+ export declare function fromPath(path: string, options?: {
8
+ type?: string;
9
+ }): Promise<File>;
10
+ /**
11
+ * A file on disk to download into. The bytes are written beside it, under
12
+ * the same name and ".part", and it takes the name only once every byte is
13
+ * there: a download that fails leaves nothing, and never a file with holes
14
+ * where it was meant to be.
15
+ */
16
+ export declare function toPath(path: string): Promise<Sink>;
package/dist/node.js ADDED
@@ -0,0 +1,51 @@
1
+ // Files on disk, for a program in Node: one to upload, and one to download
2
+ // into. Apart from the main entry, which runs in browsers too.
3
+ //
4
+ // import { fromPath, toPath } from "@filefreak.io/sdk/node";
5
+ // const item = await ff.upload(await fromPath("film.mp4"));
6
+ // await ff.download(item.id, await toPath("copy.mp4"));
7
+ import { openAsBlob } from "node:fs";
8
+ import { open, rename, rm } from "node:fs/promises";
9
+ import { basename } from "node:path";
10
+ /**
11
+ * A file on disk, to upload: read a part at a time as it is sent, never held
12
+ * whole. Node refuses to read it if it changes on disk in the meantime, so a
13
+ * file is never sent half old and half new.
14
+ */
15
+ export async function fromPath(path, options = {}) {
16
+ const blob = await openAsBlob(path, options.type ? { type: options.type } : {});
17
+ return new File([blob], basename(path), { type: blob.type });
18
+ }
19
+ /**
20
+ * A file on disk to download into. The bytes are written beside it, under
21
+ * the same name and ".part", and it takes the name only once every byte is
22
+ * there: a download that fails leaves nothing, and never a file with holes
23
+ * where it was meant to be.
24
+ */
25
+ export async function toPath(path) {
26
+ const partial = `${path}.part`;
27
+ const handle = await open(partial, "w");
28
+ let shut = false;
29
+ const close = async () => {
30
+ if (shut)
31
+ return;
32
+ shut = true;
33
+ await handle.close();
34
+ };
35
+ return {
36
+ async write(position, data) {
37
+ for (let at = 0; at < data.byteLength;) {
38
+ const { bytesWritten } = await handle.write(data, at, data.byteLength - at, position + at);
39
+ at += bytesWritten;
40
+ }
41
+ },
42
+ async close() {
43
+ await close();
44
+ await rename(partial, path);
45
+ },
46
+ async abort() {
47
+ await close();
48
+ await rm(partial, { force: true });
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,34 @@
1
+ /** At most this many ranges at once. Six filled the line measured; more only adds requests. */
2
+ export declare const RANGES_AT_ONCE = 6;
3
+ /** No range is cut smaller than this: each is a request, and a request costs a round trip before its first byte. */
4
+ export declare const LEAST_RANGE: number;
5
+ /** Where a download's bytes go. */
6
+ export interface Sink {
7
+ /** Write `data` at `position` in the file. Called for several positions at once. */
8
+ write(position: number, data: Uint8Array<ArrayBuffer>): Promise<void>;
9
+ /** Called once every byte has been written. */
10
+ close?(): Promise<void>;
11
+ /** Called instead of close when the download fails or is stopped: a file with holes in it is worse than none. */
12
+ abort?(reason: unknown): Promise<void>;
13
+ }
14
+ export interface RangesOptions {
15
+ url: string;
16
+ sizeBytes: number;
17
+ sink: Sink;
18
+ signal?: AbortSignal | undefined;
19
+ /** How many bytes have been written, after each write. */
20
+ progress?: ((writtenBytes: number) => void) | undefined;
21
+ /** At most this many ranges at once. */
22
+ ranges?: number | undefined;
23
+ fetch?: typeof fetch;
24
+ sleep?: (ms: number) => Promise<void>;
25
+ quietMs?: number;
26
+ /** LEAST_RANGE, unless a test makes it smaller rather than make a file of gigabytes. */
27
+ leastRange?: number;
28
+ }
29
+ /**
30
+ * Fetch `url` as ranges and write each where it belongs. Resolves when every
31
+ * byte has been written; rejects, with every request stopped, when one range
32
+ * cannot be finished.
33
+ */
34
+ export declare function fetchRanges(options: RangesOptions): Promise<void>;