@filefreak.io/sdk 0.3.2 → 0.4.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.
- package/README.md +36 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.js +7 -2
- package/dist/frames.d.ts +21 -0
- package/dist/frames.js +124 -0
- package/dist/generated.d.ts +27 -0
- package/dist/generated.js +9 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/uploads.d.ts +9 -1
- package/dist/uploads.js +11 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,6 +74,42 @@ folder asked for, for a minute to an hour. It is checked against its key on
|
|
|
74
74
|
every request, so revoking the key stops its tokens at once. A token cannot
|
|
75
75
|
make another.
|
|
76
76
|
|
|
77
|
+
## Previews
|
|
78
|
+
|
|
79
|
+
Once the service has made them, within a few minutes of an upload, a file
|
|
80
|
+
has `thumbnailUrl`, a small WebP picture of an image, a photo from an
|
|
81
|
+
iPhone, a PDF's first page, the cover in an audio file's tags or a frame of
|
|
82
|
+
a video; or `previewTextUrl`, the first lines of a text or code file or a
|
|
83
|
+
document, as text. Both are addresses on storage itself that need no key.
|
|
84
|
+
Each is the same all day, so a browser keeps what it fetched, and fetching
|
|
85
|
+
one counts as nothing. They are null until made, and for a file with none.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const { items } = await ff.files.list({ parent: folder.id });
|
|
89
|
+
for (const item of items) {
|
|
90
|
+
if (item.thumbnailUrl) gallery.append(Object.assign(new Image(), { src: item.thumbnailUrl, alt: item.name }));
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The service never reads a video, so a video's picture is a frame of it that
|
|
95
|
+
you send. In a web page, `upload` draws one about a second in from the file
|
|
96
|
+
the page has, and sends it once the file is stored, without the upload
|
|
97
|
+
waiting for it; `frame: false` sends none. On a server, make a JPEG of a
|
|
98
|
+
frame with anything that can, and send that:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { execFile } from "node:child_process";
|
|
102
|
+
import { readFile } from "node:fs/promises";
|
|
103
|
+
import { promisify } from "node:util";
|
|
104
|
+
|
|
105
|
+
// The frame a second in, 960 pixels wide, as a JPEG.
|
|
106
|
+
await promisify(execFile)("ffmpeg", ["-ss", "1", "-i", "film.mp4", "-frames:v", "1", "-vf", "scale=960:-2", "-y", "frame.jpg"]);
|
|
107
|
+
await ff.files.sendFrame(file.id, new Blob([await readFile("frame.jpg")]));
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
A video's picture is a JPEG of at most 2 MB, sent once. A file under Shard
|
|
111
|
+
Protection has no previews.
|
|
112
|
+
|
|
77
113
|
## Saving from a link
|
|
78
114
|
|
|
79
115
|
`get` saves what a share link or a public page holds. It needs no key, and
|
package/dist/client.d.ts
CHANGED
|
@@ -36,6 +36,13 @@ export declare class FileFreak {
|
|
|
36
36
|
readonly files: Api["files"] & {
|
|
37
37
|
/** Everything in a folder, read to the end a page at a time. */
|
|
38
38
|
each(query?: Omit<FilesListQuery, "cursor">, options?: CallOptions): AsyncIterable<Item>;
|
|
39
|
+
/**
|
|
40
|
+
* Give a video its picture in listings: a JPEG of one of its frames, at
|
|
41
|
+
* most 2 MB, which the service makes small and gives as the video's
|
|
42
|
+
* thumbnailUrl within a few minutes. Once for each video. In a web page,
|
|
43
|
+
* upload() does it itself.
|
|
44
|
+
*/
|
|
45
|
+
sendFrame(id: string, jpeg: Blob, options?: CallOptions): Promise<void>;
|
|
39
46
|
};
|
|
40
47
|
readonly folders: Api["folders"];
|
|
41
48
|
readonly tree: Api["tree"] & {
|
package/dist/client.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// otherwise write around them: sending a file in parts, fetching one in
|
|
4
4
|
// ranges, and reading a listing or the changes to the end.
|
|
5
5
|
import { bind, OPERATIONS, } from "./generated.js";
|
|
6
|
+
import { sendFrame } from "./frames.js";
|
|
6
7
|
import { fetchRanges } from "./ranges.js";
|
|
7
8
|
import { asItIsKept, readProtected } from "./reader.js";
|
|
8
9
|
import { FileFreakError, sleep, Transport } from "./transport.js";
|
|
@@ -57,7 +58,11 @@ export class FileFreak {
|
|
|
57
58
|
constructor(options) {
|
|
58
59
|
this.transport = new Transport(options);
|
|
59
60
|
const api = bind((operation, input, callOptions) => this.call(operation, input, callOptions));
|
|
60
|
-
this.files = {
|
|
61
|
+
this.files = {
|
|
62
|
+
...api.files,
|
|
63
|
+
each: (query = {}, callOptions) => pages(api.files.list, query, callOptions),
|
|
64
|
+
sendFrame: (id, jpeg, callOptions) => sendFrame(api.files.frame, this.transport.fetch, id, jpeg, callOptions),
|
|
65
|
+
};
|
|
61
66
|
this.folders = api.folders;
|
|
62
67
|
this.tree = { ...api.tree, walk: (query = {}, callOptions) => new Walk(api.tree.list, query, callOptions) };
|
|
63
68
|
this.changes = { ...api.changes, follow: (cursor, followOptions = {}) => follow(api.changes.list, cursor ?? null, followOptions) };
|
|
@@ -94,7 +99,7 @@ export class FileFreak {
|
|
|
94
99
|
* to storage. See UploadOptions, and UploadStopped for carrying on.
|
|
95
100
|
*/
|
|
96
101
|
upload(source, options = {}) {
|
|
97
|
-
return upload({ ...this.uploads, file: this.files.get }, this.transport.fetch, source, options);
|
|
102
|
+
return upload({ ...this.uploads, file: this.files.get, frame: this.files.frame }, this.transport.fetch, source, options);
|
|
98
103
|
}
|
|
99
104
|
/**
|
|
100
105
|
* Fetch a file straight from storage into `sink`: as up to six ranges at
|
package/dist/frames.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FilesFrameAnswer } from "./generated.js";
|
|
2
|
+
import { type CallOptions } from "./transport.js";
|
|
3
|
+
/** The most the service keeps of a picture of a video. Anything larger is not made into one. */
|
|
4
|
+
export declare const FRAME_MAX_BYTES = 2000000;
|
|
5
|
+
/** Whether the service takes a picture for a file of this type and name. Its word is final: files.frame refuses any other with 409 no_frame_wanted. */
|
|
6
|
+
export declare function takesFrame(contentType: string | null, name: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Sends a JPEG of a video's frame to be its picture: asks files.frame where,
|
|
9
|
+
* and puts it there. Throws if it is not a JPEG of at most FRAME_MAX_BYTES,
|
|
10
|
+
* if the service wants none for the file (409 no_frame_wanted: not a video,
|
|
11
|
+
* protected, or given one already), or if storage refuses it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function sendFrame(frame: (id: string, options?: CallOptions) => Promise<FilesFrameAnswer>, fetcher: typeof fetch, id: string, jpeg: Blob, options?: CallOptions): Promise<void>;
|
|
14
|
+
/** Whether this is a web page, which can draw a video. */
|
|
15
|
+
export declare function canDrawFrames(): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* A frame about a second in, or halfway through a shorter video, as a JPEG,
|
|
18
|
+
* or null where this browser cannot play the video or draw it. A second in
|
|
19
|
+
* rather than the first frame, which is black more often than not.
|
|
20
|
+
*/
|
|
21
|
+
export declare function drawFrame(video: Blob): Promise<Blob | null>;
|
package/dist/frames.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// A picture of a video, for its place in a listing (an item's thumbnailUrl).
|
|
2
|
+
//
|
|
3
|
+
// The service never reads a video: making a picture of one would take a
|
|
4
|
+
// video decoder reading anybody's upload where the service's keys are.
|
|
5
|
+
// Whoever has the video in hand draws a frame and sends it, as the website
|
|
6
|
+
// does; files.frame says where, and the service makes it small and keeps
|
|
7
|
+
// it. In a web page, upload() does this itself once a video is stored, from
|
|
8
|
+
// the file the page already has, so nothing is fetched. Anywhere else, a
|
|
9
|
+
// program that can make a JPEG of a frame (with ffmpeg, say) sends it with
|
|
10
|
+
// files.sendFrame.
|
|
11
|
+
import { FileFreakError } from "./transport.js";
|
|
12
|
+
/** The most the service keeps of a picture of a video. Anything larger is not made into one. */
|
|
13
|
+
export const FRAME_MAX_BYTES = 2_000_000;
|
|
14
|
+
/** The longest side of a frame drawn here, in pixels. The service makes it smaller again. */
|
|
15
|
+
const EDGE = 960;
|
|
16
|
+
/** Long enough for a browser to load the start of a video it can play. One it cannot is given up on. */
|
|
17
|
+
const WAIT_MS = 15_000;
|
|
18
|
+
/**
|
|
19
|
+
* Videos by name, as the service knows them, for one typed as nothing in
|
|
20
|
+
* particular. Not by video/mp2t, which is how browsers type a TypeScript
|
|
21
|
+
* file (.ts).
|
|
22
|
+
*/
|
|
23
|
+
const VIDEO_EXTENSIONS = new Set(["mp4", "m4v", "webm", "mov", "mkv", "avi", "ogv"]);
|
|
24
|
+
/** Whether the service takes a picture for a file of this type and name. Its word is final: files.frame refuses any other with 409 no_frame_wanted. */
|
|
25
|
+
export function takesFrame(contentType, name) {
|
|
26
|
+
const type = (contentType ?? "").toLowerCase();
|
|
27
|
+
const dot = name.lastIndexOf(".");
|
|
28
|
+
const ext = dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
|
|
29
|
+
return (type.startsWith("video/") && type !== "video/mp2t") || VIDEO_EXTENSIONS.has(ext);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Sends a JPEG of a video's frame to be its picture: asks files.frame where,
|
|
33
|
+
* and puts it there. Throws if it is not a JPEG of at most FRAME_MAX_BYTES,
|
|
34
|
+
* if the service wants none for the file (409 no_frame_wanted: not a video,
|
|
35
|
+
* protected, or given one already), or if storage refuses it.
|
|
36
|
+
*/
|
|
37
|
+
export async function sendFrame(frame, fetcher, id, jpeg, options = {}) {
|
|
38
|
+
const start = new Uint8Array(await jpeg.slice(0, 3).arrayBuffer());
|
|
39
|
+
if (jpeg.size > FRAME_MAX_BYTES || start[0] !== 0xff || start[1] !== 0xd8 || start[2] !== 0xff) {
|
|
40
|
+
throw new TypeError(`A video's picture is a JPEG of at most ${FRAME_MAX_BYTES} bytes.`);
|
|
41
|
+
}
|
|
42
|
+
const to = await frame(id, options);
|
|
43
|
+
let res;
|
|
44
|
+
try {
|
|
45
|
+
res = await fetcher(to.url, { method: "PUT", headers: to.headers, body: jpeg, signal: options.signal });
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
if (options.signal?.aborted)
|
|
49
|
+
throw options.signal.reason;
|
|
50
|
+
throw new FileFreakError(0, "network_error", `Storage could not be reached: ${e instanceof Error ? e.message : String(e)}.`);
|
|
51
|
+
}
|
|
52
|
+
await res.body?.cancel().catch(() => undefined);
|
|
53
|
+
if (!res.ok)
|
|
54
|
+
throw new FileFreakError(res.status, "storage_refused", `Storage refused the picture with ${res.status}.`);
|
|
55
|
+
}
|
|
56
|
+
/** The page this runs in, or null outside a web page: in Node, or in a worker. */
|
|
57
|
+
function page() {
|
|
58
|
+
const document = globalThis.document;
|
|
59
|
+
return typeof document?.createElement === "function" ? document : null;
|
|
60
|
+
}
|
|
61
|
+
/** Whether this is a web page, which can draw a video. */
|
|
62
|
+
export function canDrawFrames() {
|
|
63
|
+
return page() !== null && typeof URL.createObjectURL === "function";
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A frame about a second in, or halfway through a shorter video, as a JPEG,
|
|
67
|
+
* or null where this browser cannot play the video or draw it. A second in
|
|
68
|
+
* rather than the first frame, which is black more often than not.
|
|
69
|
+
*/
|
|
70
|
+
export async function drawFrame(video) {
|
|
71
|
+
const document = page();
|
|
72
|
+
if (!document)
|
|
73
|
+
return null;
|
|
74
|
+
const element = document.createElement("video");
|
|
75
|
+
const src = URL.createObjectURL(video);
|
|
76
|
+
element.muted = true;
|
|
77
|
+
element.playsInline = true;
|
|
78
|
+
element.preload = "auto";
|
|
79
|
+
try {
|
|
80
|
+
await within(new Promise((resolve, reject) => {
|
|
81
|
+
element.addEventListener("loadedmetadata", () => resolve(), { once: true });
|
|
82
|
+
element.addEventListener("error", () => reject(new Error("cannot play")), { once: true });
|
|
83
|
+
element.src = src;
|
|
84
|
+
}));
|
|
85
|
+
const at = Number.isFinite(element.duration) && element.duration > 0 ? Math.min(1, element.duration / 2) : 0;
|
|
86
|
+
await within(new Promise((resolve, reject) => {
|
|
87
|
+
element.addEventListener("seeked", () => resolve(), { once: true });
|
|
88
|
+
element.addEventListener("error", () => reject(new Error("cannot seek")), { once: true });
|
|
89
|
+
element.currentTime = at;
|
|
90
|
+
}));
|
|
91
|
+
if (!element.videoWidth || !element.videoHeight)
|
|
92
|
+
return null;
|
|
93
|
+
const scale = Math.min(1, EDGE / Math.max(element.videoWidth, element.videoHeight));
|
|
94
|
+
const canvas = document.createElement("canvas");
|
|
95
|
+
canvas.width = Math.max(1, Math.round(element.videoWidth * scale));
|
|
96
|
+
canvas.height = Math.max(1, Math.round(element.videoHeight * scale));
|
|
97
|
+
const context = canvas.getContext("2d");
|
|
98
|
+
if (!context)
|
|
99
|
+
return null;
|
|
100
|
+
context.drawImage(element, 0, 0, canvas.width, canvas.height);
|
|
101
|
+
return await new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", 0.85));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// A video this browser cannot play.
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
element.removeAttribute("src");
|
|
109
|
+
element.load();
|
|
110
|
+
URL.revokeObjectURL(src);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function within(work) {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const timer = setTimeout(() => reject(new Error("too slow")), WAIT_MS);
|
|
116
|
+
work.then((value) => {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
resolve(value);
|
|
119
|
+
}, (e) => {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
package/dist/generated.d.ts
CHANGED
|
@@ -23,6 +23,10 @@ export interface Item {
|
|
|
23
23
|
revision: number;
|
|
24
24
|
/** The SHA-256 the uploader gave when it completed the upload of this content, in lower-case hex, or null. The uploader's word: this service never sees a file's bytes and does not check it. */
|
|
25
25
|
clientSha256: string | null;
|
|
26
|
+
/** A small picture of the file, as WebP no more than 480 pixels on its longest side, once the service has made one: of an image, a photo from an iPhone, a PDF's first page, the cover in an audio file's tags, or a frame of a video, sent with files.frame. Made within a few minutes of an upload. The address is on storage itself and needs no key. It is the same for the rest of the day, so that it can be cached, and works for a day after that. Fetching it counts as nothing. Null for a folder, and for a file with none. */
|
|
27
|
+
thumbnailUrl: string | null;
|
|
28
|
+
/** The first lines of a text or code file, a Word or OpenDocument text, a deck's first slide or the top left of a spreadsheet's first sheet, as UTF-8 text, once the service has made them, at an address like thumbnailUrl's. Never of a file whose name says it holds keys or passwords. Null otherwise. */
|
|
29
|
+
previewTextUrl: string | null;
|
|
26
30
|
/** Whether the file is under Shard Protection: kept as sealed fragments, and read through its map (files.shards) rather than from an address. */
|
|
27
31
|
protected: boolean;
|
|
28
32
|
/** Its own Shard Protection setting. inherit follows the nearest folder above it that has one, and with none the drive's default: the app's, which its owner turns on from the app's page, or for a device the account's. The nearest setting wins. A file is converted within a few minutes of a change, and protected says when it has been. */
|
|
@@ -387,6 +391,15 @@ export type UploadsCompleteAnswer = {
|
|
|
387
391
|
} | Pending;
|
|
388
392
|
/** What DELETE /v1/uploads/:id answers with. */
|
|
389
393
|
export type UploadsCancelAnswer = Ok;
|
|
394
|
+
/** What POST /v1/files/:id/frame answers with. */
|
|
395
|
+
export type FilesFrameAnswer = {
|
|
396
|
+
/** Where to PUT the JPEG. Needs no key: the address is the permission. */
|
|
397
|
+
url: string;
|
|
398
|
+
/** Headers the request must carry. */
|
|
399
|
+
headers: Record<string, string>;
|
|
400
|
+
/** When the address stops working. */
|
|
401
|
+
expiresAt: string;
|
|
402
|
+
};
|
|
390
403
|
/** What GET /v1/files/:id/download takes in its query. */
|
|
391
404
|
export type FilesDownloadUrlQuery = {
|
|
392
405
|
/** true to have a browser show it, for the types that are safe to show. Anything else is saved, whatever is asked. */
|
|
@@ -700,6 +713,12 @@ export declare const OPERATIONS: {
|
|
|
700
713
|
readonly idempotent: false;
|
|
701
714
|
readonly repeatable: false;
|
|
702
715
|
};
|
|
716
|
+
readonly "files.frame": {
|
|
717
|
+
readonly method: "POST";
|
|
718
|
+
readonly path: "/v1/files/:id/frame";
|
|
719
|
+
readonly idempotent: false;
|
|
720
|
+
readonly repeatable: false;
|
|
721
|
+
};
|
|
703
722
|
readonly "files.downloadUrl": {
|
|
704
723
|
readonly method: "GET";
|
|
705
724
|
readonly path: "/v1/files/:id/download";
|
|
@@ -870,6 +889,14 @@ export declare function bind(call: Call): {
|
|
|
870
889
|
* POST /v1/files/:id/restore. Needs the scope files:write.
|
|
871
890
|
*/
|
|
872
891
|
restore: (id: string, options?: CallOptions) => Promise<FilesRestoreAnswer>;
|
|
892
|
+
/**
|
|
893
|
+
* Give a video its picture.
|
|
894
|
+
*
|
|
895
|
+
* The service never reads a video, so a video's picture (its thumbnailUrl) is a frame of it that a program sends. This answers where to send one: PUT a JPEG of at most 2 MB to url, with headers, within 2 minutes, so have it ready before asking. The service makes it small and keeps it, and the video's thumbnailUrl gives it within a few minutes. Anything else sent is not made into a picture. Once for each video: one that has been sent a picture, or was given an address for one in the last few minutes, is answered with 409 no_frame_wanted. In a web page, the SDK's upload draws a frame about a second in and sends it itself; anywhere else, the SDK's files.sendFrame sends a JPEG made by anything that can make one, ffmpeg for one. A file under Shard Protection is given no picture, and is answered with 409 no_frame_wanted.
|
|
896
|
+
*
|
|
897
|
+
* POST /v1/files/:id/frame. Needs the scope files:write.
|
|
898
|
+
*/
|
|
899
|
+
frame: (id: string, options?: CallOptions) => Promise<FilesFrameAnswer>;
|
|
873
900
|
/**
|
|
874
901
|
* An address to download a file from.
|
|
875
902
|
*
|
package/dist/generated.js
CHANGED
|
@@ -20,6 +20,7 @@ export const OPERATIONS = {
|
|
|
20
20
|
"uploads.fragments": { method: "POST", path: "/v1/uploads/:id/fragments", idempotent: false, repeatable: true },
|
|
21
21
|
"uploads.complete": { method: "POST", path: "/v1/uploads/:id/complete", idempotent: false, repeatable: true },
|
|
22
22
|
"uploads.cancel": { method: "DELETE", path: "/v1/uploads/:id", idempotent: false, repeatable: false },
|
|
23
|
+
"files.frame": { method: "POST", path: "/v1/files/:id/frame", idempotent: false, repeatable: false },
|
|
23
24
|
"files.downloadUrl": { method: "GET", path: "/v1/files/:id/download", idempotent: false, repeatable: false },
|
|
24
25
|
"files.shards": { method: "GET", path: "/v1/files/:id/shards", idempotent: false, repeatable: false },
|
|
25
26
|
"files.image": { method: "GET", path: "/v1/files/:id/image", idempotent: false, repeatable: false },
|
|
@@ -93,6 +94,14 @@ export function bind(call) {
|
|
|
93
94
|
* POST /v1/files/:id/restore. Needs the scope files:write.
|
|
94
95
|
*/
|
|
95
96
|
restore: (id, options) => call("files.restore", { params: { id } }, options),
|
|
97
|
+
/**
|
|
98
|
+
* Give a video its picture.
|
|
99
|
+
*
|
|
100
|
+
* The service never reads a video, so a video's picture (its thumbnailUrl) is a frame of it that a program sends. This answers where to send one: PUT a JPEG of at most 2 MB to url, with headers, within 2 minutes, so have it ready before asking. The service makes it small and keeps it, and the video's thumbnailUrl gives it within a few minutes. Anything else sent is not made into a picture. Once for each video: one that has been sent a picture, or was given an address for one in the last few minutes, is answered with 409 no_frame_wanted. In a web page, the SDK's upload draws a frame about a second in and sends it itself; anywhere else, the SDK's files.sendFrame sends a JPEG made by anything that can make one, ffmpeg for one. A file under Shard Protection is given no picture, and is answered with 409 no_frame_wanted.
|
|
101
|
+
*
|
|
102
|
+
* POST /v1/files/:id/frame. Needs the scope files:write.
|
|
103
|
+
*/
|
|
104
|
+
frame: (id, options) => call("files.frame", { params: { id } }, options),
|
|
96
105
|
/**
|
|
97
106
|
* An address to download a file from.
|
|
98
107
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { FileFreak, Walk, type DownloadOptions, type FollowOptions } from "./client.js";
|
|
2
2
|
export * from "./generated.js";
|
|
3
|
+
export { FRAME_MAX_BYTES, takesFrame } from "./frames.js";
|
|
3
4
|
export { get, type GetOptions, type Got, type Into, type LinkedFile } from "./links.js";
|
|
4
5
|
export { type Sink } from "./ranges.js";
|
|
5
6
|
export { checkOutline, fileKeys, MOST_BLOCK_BYTES, openBlock, openFragment, outlineMac, sealBlock, sealFragment, SHARD_OVERHEAD, SHARD_VERSION, SHARD_VERSION_2, ShardError, shardKey, ShardLayout, type FileKeys, type FragmentAt, type Outline, type OutlineBlock, type ShardErrorCode, type ShardKey, } from "./shards.js";
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// await get("https://filefreak.link/share/...", toFolder("downloads"));
|
|
12
12
|
export { FileFreak, Walk } from "./client.js";
|
|
13
13
|
export * from "./generated.js";
|
|
14
|
+
export { FRAME_MAX_BYTES, takesFrame } from "./frames.js";
|
|
14
15
|
export { get } from "./links.js";
|
|
15
16
|
export { checkOutline, fileKeys, MOST_BLOCK_BYTES, openBlock, openFragment, outlineMac, sealBlock, sealFragment, SHARD_OVERHEAD, SHARD_VERSION, SHARD_VERSION_2, ShardError, shardKey, ShardLayout, } from "./shards.js";
|
|
16
17
|
export { FileFreakError } from "./transport.js";
|
package/dist/uploads.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FilesGetAnswer, Item, Upload, UploadsBeginAnswer, UploadsBeginBody, UploadsCompleteAnswer, UploadsCompleteBody, UploadsFragmentsAnswer, UploadsFragmentsBody, UploadsGetAnswer, UploadsPartsAnswer, UploadsPartsQuery } from "./generated.js";
|
|
1
|
+
import type { FilesFrameAnswer, FilesGetAnswer, Item, Upload, UploadsBeginAnswer, UploadsBeginBody, UploadsCompleteAnswer, UploadsCompleteBody, UploadsFragmentsAnswer, UploadsFragmentsBody, UploadsGetAnswer, UploadsPartsAnswer, UploadsPartsQuery } from "./generated.js";
|
|
2
2
|
import { type CallOptions } from "./transport.js";
|
|
3
3
|
/** What can be uploaded. A File's name and type are used when none are given. */
|
|
4
4
|
export type Source = Blob | ArrayBuffer | Uint8Array;
|
|
@@ -34,6 +34,13 @@ export interface UploadOptions {
|
|
|
34
34
|
signal?: AbortSignal;
|
|
35
35
|
/** For beginning: the caller's own key, so that a begin sent again makes one upload. */
|
|
36
36
|
idempotencyKey?: string;
|
|
37
|
+
/**
|
|
38
|
+
* In a web page, a video is given its picture in listings (its
|
|
39
|
+
* thumbnailUrl): a frame about a second in, drawn from the file here and
|
|
40
|
+
* sent once it is stored, without the upload waiting for it. false sends
|
|
41
|
+
* none. Anywhere else nothing is drawn: send one with files.sendFrame.
|
|
42
|
+
*/
|
|
43
|
+
frame?: boolean;
|
|
37
44
|
}
|
|
38
45
|
/** An upload that stopped part way. What arrived stays in storage until the upload expires: carry on with `resume: uploadId`, or cancel it. */
|
|
39
46
|
export declare class UploadStopped extends Error {
|
|
@@ -48,6 +55,7 @@ export interface UploadCalls {
|
|
|
48
55
|
fragments(id: string, body: UploadsFragmentsBody, options?: CallOptions): Promise<UploadsFragmentsAnswer>;
|
|
49
56
|
complete(id: string, body?: UploadsCompleteBody, options?: CallOptions): Promise<UploadsCompleteAnswer>;
|
|
50
57
|
file(id: string, options?: CallOptions): Promise<FilesGetAnswer>;
|
|
58
|
+
frame(id: string, options?: CallOptions): Promise<FilesFrameAnswer>;
|
|
51
59
|
}
|
|
52
60
|
/** The part size to ask for: 32 MiB, halved while that would leave fewer parts than are sent at once. */
|
|
53
61
|
export declare function partSizeFor(fileBytes: number, atOnce: number): number;
|
package/dist/uploads.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
// Carrying on, every fragment not recorded is confirmed before it is sent
|
|
31
31
|
// again, since storage may hold it already. A fragment is up to 64 MiB and
|
|
32
32
|
// sealed in memory, so the memory a sealed upload needs is the same.
|
|
33
|
+
import { canDrawFrames, drawFrame, sendFrame, takesFrame } from "./frames.js";
|
|
33
34
|
import { base64, Md5 } from "./md5.js";
|
|
34
35
|
import { sealFragment, ShardError, shardKey, ShardLayout } from "./shards.js";
|
|
35
36
|
import { FileFreakError, sleep } from "./transport.js";
|
|
@@ -170,16 +171,25 @@ export async function upload(calls, fetcher, source, options = {}) {
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
options.onBegun?.(job.upload);
|
|
174
|
+
let item;
|
|
173
175
|
try {
|
|
174
176
|
tally(job, options);
|
|
175
177
|
await deliver(calls, fetcher, job, atOnce, options);
|
|
176
|
-
|
|
178
|
+
item = await finish(calls, fetcher, job, atOnce, options);
|
|
177
179
|
}
|
|
178
180
|
catch (e) {
|
|
179
181
|
if (signal?.aborted)
|
|
180
182
|
throw signal.reason;
|
|
181
183
|
throw new UploadStopped(job.upload.id, e);
|
|
182
184
|
}
|
|
185
|
+
if (options.frame !== false && source instanceof Blob && !item.protected && takesFrame(item.contentType, item.name) && canDrawFrames()) {
|
|
186
|
+
// Not waited for, and nothing said if it fails: the file is stored, and
|
|
187
|
+
// without a picture a video is listed as it was before there were any.
|
|
188
|
+
void drawFrame(source)
|
|
189
|
+
.then((jpeg) => (jpeg ? sendFrame(calls.frame, fetcher, item.id, jpeg) : undefined))
|
|
190
|
+
.catch(() => undefined);
|
|
191
|
+
}
|
|
192
|
+
return item;
|
|
183
193
|
}
|
|
184
194
|
function tally(job, options) {
|
|
185
195
|
let sent = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@filefreak.io/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "FileFreak's developer API for Node and the browser: every endpoint, uploads straight to storage in parts, downloads in ranges, and the changes to follow.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://filefreak.io/developers/reference",
|