@editframe/api 0.10.0-beta.7 → 0.11.0-beta.10

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.
@@ -30,4 +30,4 @@ export interface CreateImageFileResult {
30
30
  asset_id: string;
31
31
  }
32
32
  export declare const createImageFile: (client: Client, payload: z.infer<typeof CreateImageFilePayload>) => Promise<CreateImageFileResult>;
33
- export declare const uploadImageFile: (client: Client, fileId: string, fileStream: Readable, fileSize: number) => Promise<void>;
33
+ export declare const uploadImageFile: (client: Client, fileId: string, fileStream: Readable, fileSize: number) => import('../uploadChunks.ts').IteratorWithPromise<import('../uploadChunks.ts').UploadChunkEvent>;
@@ -1,5 +1,5 @@
1
- import { z } from "zod";
2
1
  import debug from "debug";
2
+ import { z } from "zod";
3
3
  import { uploadChunks } from "../uploadChunks.js";
4
4
  const log = debug("ef:api:image-file");
5
5
  const MAX_IMAGE_SIZE = 1024 * 1024 * 16;
@@ -26,20 +26,14 @@ const createImageFile = async (client, payload) => {
26
26
  `Failed to create file ${response.status} ${response.statusText}`
27
27
  );
28
28
  };
29
- const uploadImageFile = async (client, fileId, fileStream, fileSize) => {
29
+ const uploadImageFile = (client, fileId, fileStream, fileSize) => {
30
30
  log("Uploading image file", fileId);
31
- if (fileSize > MAX_IMAGE_SIZE) {
32
- throw new Error(
33
- `File size ${fileSize} bytes exceeds limit ${MAX_IMAGE_SIZE} bytes`
34
- );
35
- }
36
- const result = await uploadChunks(client, {
31
+ return uploadChunks(client, {
37
32
  url: `/api/v1/image_files/${fileId}/upload`,
38
33
  fileSize,
39
- fileStream
34
+ fileStream,
35
+ maxSize: MAX_IMAGE_SIZE
40
36
  });
41
- log("Image file upload complete");
42
- return result;
43
37
  };
44
38
  export {
45
39
  CreateImageFilePayload,
@@ -269,4 +269,4 @@ export interface CreateISOBMFFTrackResult {
269
269
  complete: boolean;
270
270
  }
271
271
  export declare const createISOBMFFTrack: (client: Client, payload: z.infer<typeof CreateISOBMFFTrackPayload>) => Promise<CreateISOBMFFTrackResult>;
272
- export declare const uploadISOBMFFTrack: (client: Client, fileId: string, trackId: number, fileStream: Readable, trackSize: number) => Promise<void>;
272
+ export declare const uploadISOBMFFTrack: (client: Client, fileId: string, trackId: number, fileStream: Readable, trackSize: number) => import('../uploadChunks.ts').IteratorWithPromise<import('../uploadChunks.ts').UploadChunkEvent>;
@@ -39,14 +39,14 @@ const createISOBMFFTrack = async (client, payload) => {
39
39
  `Failed to create isobmff track ${response.status} ${response.statusText}`
40
40
  );
41
41
  };
42
- const uploadISOBMFFTrack = async (client, fileId, trackId, fileStream, trackSize) => {
42
+ const uploadISOBMFFTrack = (client, fileId, trackId, fileStream, trackSize) => {
43
43
  log("Uploading fragment track", fileId);
44
- await uploadChunks(client, {
44
+ return uploadChunks(client, {
45
45
  url: `/api/v1/isobmff_tracks/${fileId}/${trackId}/upload`,
46
46
  fileStream,
47
- fileSize: trackSize
47
+ fileSize: trackSize,
48
+ maxSize: MAX_TRACK_SIZE
48
49
  });
49
- log("Fragment track upload complete");
50
50
  };
51
51
  export {
52
52
  CreateISOBMFFTrackPayload,
@@ -1,6 +1,7 @@
1
1
  import { Readable } from 'node:stream';
2
2
  import { z } from 'zod';
3
3
  import { Client } from '../client.ts';
4
+ import { IteratorWithPromise, UploadChunkEvent } from '../uploadChunks.ts';
4
5
  declare const FileProcessors: z.ZodEffects<z.ZodArray<z.ZodUnion<[z.ZodLiteral<"isobmff">, z.ZodLiteral<"image">, z.ZodLiteral<"captions">, z.ZodString]>, "many">, string[], string[]>;
5
6
  export declare const CreateUnprocessedFilePayload: z.ZodObject<{
6
7
  md5: z.ZodString;
@@ -45,9 +46,33 @@ export interface UpdateUnprocessedFileResult {
45
46
  }
46
47
  export declare const createUnprocessedFile: (client: Client, payload: z.infer<typeof CreateUnprocessedFilePayload>) => Promise<CreateUnprocessedFileResult>;
47
48
  export declare const updateUnprocessedFile: (client: Client, fileId: string, payload: Partial<z.infer<typeof UpdateUnprocessedFilePayload>>) => Promise<UpdateUnprocessedFileResult>;
48
- export declare const uploadUnprocessedFile: (client: Client, fileId: string, fileStream: Readable, fileSize: number) => Promise<void>;
49
- export declare const processAVFileBuffer: (client: Client, buffer: Buffer, filename?: string) => Promise<UpdateUnprocessedFileResult>;
50
- export declare const processAVFile: (client: Client, filePath: string) => Promise<UpdateUnprocessedFileResult>;
51
- export declare const processImageFileBuffer: (client: Client, buffer: Buffer, filename?: string) => Promise<UpdateUnprocessedFileResult>;
52
- export declare const processImageFile: (client: Client, filePath: string) => Promise<UpdateUnprocessedFileResult>;
49
+ export declare const uploadUnprocessedFile: (client: Client, fileId: string, fileStream: Readable, fileSize: number) => IteratorWithPromise<UploadChunkEvent>;
50
+ export declare const processAVFileBuffer: (client: Client, buffer: Buffer, filename?: string) => {
51
+ progress(): AsyncGenerator<{
52
+ type: string;
53
+ progress: number;
54
+ }, void, unknown>;
55
+ file: () => Promise<UpdateUnprocessedFileResult>;
56
+ };
57
+ export declare const processAVFile: (client: Client, filePath: string) => {
58
+ progress: () => Promise<AsyncGenerator<{
59
+ type: string;
60
+ progress: number;
61
+ }, void, unknown>>;
62
+ file: () => Promise<UpdateUnprocessedFileResult>;
63
+ };
64
+ export declare const processImageFileBuffer: (client: Client, buffer: Buffer, filename?: string) => {
65
+ progress(): AsyncGenerator<{
66
+ type: string;
67
+ progress: number;
68
+ }, void, unknown>;
69
+ file: () => Promise<UpdateUnprocessedFileResult>;
70
+ };
71
+ export declare const processImageFile: (client: Client, filePath: string) => {
72
+ progress: () => Promise<AsyncGenerator<{
73
+ type: string;
74
+ progress: number;
75
+ }, void, unknown>>;
76
+ file: () => Promise<UpdateUnprocessedFileResult>;
77
+ };
53
78
  export {};
@@ -72,78 +72,95 @@ const updateUnprocessedFile = async (client, fileId, payload) => {
72
72
  `Failed to update unprocessed file ${response.status} ${response.statusText}`
73
73
  );
74
74
  };
75
- const uploadUnprocessedFile = async (client, fileId, fileStream, fileSize) => {
75
+ const uploadUnprocessedFile = (client, fileId, fileStream, fileSize) => {
76
76
  log("Uploading unprocessed file", fileId);
77
- await uploadChunks(client, {
77
+ return uploadChunks(client, {
78
78
  url: `/api/v1/unprocessed_files/${fileId}/upload`,
79
79
  fileSize,
80
- fileStream
80
+ fileStream,
81
+ maxSize: MAX_FILE_SIZE
81
82
  });
82
- log("Unprocessed file upload complete");
83
83
  };
84
- const processResource = async (client, filename, md5, byteSize, processor, doUpload) => {
84
+ const processResource = (client, filename, md5, byteSize, processor, doUpload) => {
85
85
  log("Processing", { filename, md5, byteSize, processor });
86
- const unprocessedFile = await createUnprocessedFile(client, {
86
+ const createFilePromise = createUnprocessedFile(client, {
87
87
  md5,
88
88
  processes: [],
89
89
  filename,
90
90
  byte_size: byteSize
91
91
  });
92
- if (unprocessedFile.complete === false) {
93
- await doUpload(unprocessedFile.id);
94
- }
95
- if (unprocessedFile.processes.includes(processor)) {
96
- log("File already processed", unprocessedFile);
97
- return unprocessedFile;
98
- }
99
- const fileInformation = await updateUnprocessedFile(
100
- client,
101
- unprocessedFile.id,
102
- {
103
- processes: [processor]
92
+ return {
93
+ async *progress() {
94
+ const unprocessedFile = await createFilePromise;
95
+ if (unprocessedFile.complete) {
96
+ yield { type: "progress", progress: 0 };
97
+ yield { type: "progress", progress: 1 };
98
+ }
99
+ yield* doUpload(unprocessedFile.id);
100
+ },
101
+ file: async () => {
102
+ const unprocessedFile = await createFilePromise;
103
+ if (unprocessedFile.complete) {
104
+ return unprocessedFile;
105
+ }
106
+ await doUpload(unprocessedFile.id).whenUploaded();
107
+ const fileInformation = await updateUnprocessedFile(
108
+ client,
109
+ unprocessedFile.id,
110
+ {
111
+ processes: [processor]
112
+ }
113
+ );
114
+ log("File processed", fileInformation);
115
+ return fileInformation;
104
116
  }
105
- );
106
- log("File processed", fileInformation);
107
- return fileInformation;
117
+ };
108
118
  };
109
119
  const buildBufferProcessor = (processor) => {
110
- return async (client, buffer, filename = "buffer") => {
120
+ return (client, buffer, filename = "buffer") => {
111
121
  log(`Processing file buffer: ${processor}`, filename);
112
122
  const md5 = md5Buffer(buffer);
113
- return await processResource(
123
+ return processResource(
114
124
  client,
115
125
  filename,
116
126
  md5,
117
127
  buffer.byteLength,
118
128
  processor,
119
- async (id) => {
129
+ (id) => {
120
130
  const readStream = new Readable({
121
131
  read() {
122
132
  readStream.push(buffer);
123
133
  readStream.push(null);
124
134
  }
125
135
  });
126
- await uploadUnprocessedFile(client, id, readStream, buffer.byteLength);
136
+ return uploadUnprocessedFile(client, id, readStream, buffer.byteLength);
127
137
  }
128
138
  );
129
139
  };
130
140
  };
131
141
  const buildFileProcessor = (processor) => {
132
- return async (client, filePath) => {
133
- log(`Processing file ${processor}`, filePath);
134
- const md5 = await md5FilePath(filePath);
135
- const byteSize = (await stat(filePath)).size;
136
- return await processResource(
137
- client,
138
- basename(filePath),
139
- md5,
140
- byteSize,
141
- processor,
142
- async (id) => {
143
- const readStream = createReadStream(filePath);
144
- return await uploadUnprocessedFile(client, id, readStream, byteSize);
145
- }
146
- );
142
+ return (client, filePath) => {
143
+ const processPromise = async () => {
144
+ const [md5, { size: byteSize }] = await Promise.all([
145
+ md5FilePath(filePath),
146
+ stat(filePath)
147
+ ]);
148
+ return processResource(
149
+ client,
150
+ basename(filePath),
151
+ md5,
152
+ byteSize,
153
+ processor,
154
+ (id) => {
155
+ const readStream = createReadStream(filePath);
156
+ return uploadUnprocessedFile(client, id, readStream, byteSize);
157
+ }
158
+ );
159
+ };
160
+ return {
161
+ progress: async () => (await processPromise()).progress(),
162
+ file: async () => (await processPromise()).file()
163
+ };
147
164
  };
148
165
  };
149
166
  const processAVFileBuffer = buildBufferProcessor("isobmff");
@@ -1,10 +1,19 @@
1
1
  import { Readable } from 'node:stream';
2
2
  import { Client } from './client.ts';
3
+ export interface IteratorWithPromise<T> extends AsyncGenerator<T, void, unknown> {
4
+ whenUploaded: () => Promise<T[]>;
5
+ }
6
+ export declare const fakeCompleteUpload: () => IteratorWithPromise<UploadChunkEvent>;
3
7
  interface UploadChunksOptions {
4
8
  url: string;
5
9
  fileStream: Readable;
6
10
  fileSize: number;
11
+ maxSize: number;
7
12
  chunkSizeBytes?: number;
8
13
  }
9
- export declare function uploadChunks(client: Client, { url, fileSize, fileStream, chunkSizeBytes, }: UploadChunksOptions): Promise<void>;
14
+ export interface UploadChunkEvent {
15
+ type: "progress";
16
+ progress: number;
17
+ }
18
+ export declare function uploadChunks(client: Client, { url, fileSize, fileStream, maxSize, chunkSizeBytes, }: UploadChunksOptions): IteratorWithPromise<UploadChunkEvent>;
10
19
  export {};
@@ -1,6 +1,6 @@
1
1
  import debug from "debug";
2
- import { streamChunker } from "./streamChunker.js";
3
2
  import { CHUNK_SIZE_BYTES } from "./CHUNK_SIZE_BYTES.js";
3
+ import { streamChunker } from "./streamChunker.js";
4
4
  const log = debug("ef:api:uploadChunk");
5
5
  const uploadChunk = async (client, {
6
6
  url,
@@ -34,37 +34,65 @@ const uploadChunk = async (client, {
34
34
  `Failed to upload chunk ${chunkNumber} for ${url} ${response.status} ${response.statusText}`
35
35
  );
36
36
  };
37
- async function uploadChunks(client, {
37
+ function uploadChunks(client, {
38
38
  url,
39
39
  fileSize,
40
40
  fileStream,
41
+ maxSize,
41
42
  chunkSizeBytes = CHUNK_SIZE_BYTES
42
43
  }) {
43
- log("Checking upload status", url);
44
- const uploadStatus = await client.authenticatedFetch(url);
45
- if (uploadStatus.status === 200) {
46
- log("Fragment track already uploaded");
47
- return;
48
- }
49
- let chunkNumber = 0;
50
- let complete = false;
51
- for await (const chunkBuffer of streamChunker(fileStream, chunkSizeBytes)) {
52
- log(`Uploading chunk ${chunkNumber}`);
53
- ({ complete } = await uploadChunk(client, {
54
- url,
55
- chunkBuffer,
56
- chunkNumber,
57
- fileSize,
58
- chunkSizeBytes
59
- }));
60
- chunkNumber++;
61
- }
62
- if (!fileStream.readableEnded) {
63
- throw new Error("Did not read entire file stream");
64
- }
65
- if (!complete) {
66
- throw new Error("Did not complete upload");
67
- }
44
+ const makeGenerator = async function* () {
45
+ if (fileSize > maxSize) {
46
+ throw new Error(
47
+ `File size ${fileSize} bytes exceeds limit ${maxSize} bytes`
48
+ );
49
+ }
50
+ log("Checking upload status", url);
51
+ const uploadStatus = await client.authenticatedFetch(url);
52
+ yield { type: "progress", progress: 0 };
53
+ if (uploadStatus.status === 200) {
54
+ log("Fragment track already uploaded");
55
+ yield { type: "progress", progress: 1 };
56
+ return;
57
+ }
58
+ let chunkNumber = 0;
59
+ let complete = false;
60
+ for await (const chunkBuffer of streamChunker(fileStream, chunkSizeBytes)) {
61
+ log(`Uploading chunk ${chunkNumber}`);
62
+ ({ complete } = await uploadChunk(client, {
63
+ url,
64
+ chunkBuffer,
65
+ chunkNumber,
66
+ fileSize,
67
+ chunkSizeBytes
68
+ }));
69
+ chunkNumber++;
70
+ yield {
71
+ type: "progress",
72
+ progress: Math.min(1, chunkNumber / (fileSize / chunkSizeBytes))
73
+ };
74
+ }
75
+ if (!fileStream.readableEnded) {
76
+ throw new Error("Did not read entire file stream");
77
+ }
78
+ if (!complete) {
79
+ throw new Error("Did not complete upload");
80
+ }
81
+ };
82
+ const generator = makeGenerator();
83
+ generator.whenUploaded = async () => {
84
+ if (fileSize > maxSize) {
85
+ throw new Error(
86
+ `File size ${fileSize} bytes exceeds limit ${maxSize} bytes`
87
+ );
88
+ }
89
+ const events = [];
90
+ for await (const event of generator) {
91
+ events.push(event);
92
+ }
93
+ return events;
94
+ };
95
+ return generator;
68
96
  }
69
97
  export {
70
98
  uploadChunks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/api",
3
- "version": "0.10.0-beta.7",
3
+ "version": "0.11.0-beta.10",
4
4
  "description": "API functions for EditFrame",
5
5
  "exports": {
6
6
  ".": {
@@ -28,7 +28,7 @@
28
28
  "vite-tsconfig-paths": "^4.3.2"
29
29
  },
30
30
  "dependencies": {
31
- "@editframe/assets": "0.10.0-beta.7",
31
+ "@editframe/assets": "0.11.0-beta.10",
32
32
  "debug": "^4.3.5",
33
33
  "jsonwebtoken": "^9.0.2",
34
34
  "node-fetch": "^3.3.2",
@@ -1,11 +1,11 @@
1
- import { test, expect, beforeAll, afterEach, afterAll, describe } from "vitest";
2
1
  import { http, HttpResponse } from "msw";
3
2
  import { setupServer } from "msw/node";
3
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
4
4
  import { ZodError } from "zod";
5
5
 
6
6
  import { Client } from "../client.ts";
7
- import { createImageFile, uploadImageFile } from "./image-file.ts";
8
7
  import { readableFromBuffers } from "../readableFromBuffers.ts";
8
+ import { createImageFile, uploadImageFile } from "./image-file.ts";
9
9
 
10
10
  const server = setupServer();
11
11
  const client = new Client("ef_TEST_TOKEN", "http://localhost");
@@ -96,7 +96,7 @@ describe("ImageFile", () => {
96
96
  "test-file-id",
97
97
  readableFromBuffers(Buffer.from("test")),
98
98
  1024 * 1024 * 17,
99
- ),
99
+ ).whenUploaded(),
100
100
  ).rejects.toThrowError(
101
101
  "File size 17825792 bytes exceeds limit 16777216 bytes",
102
102
  );
@@ -117,7 +117,7 @@ describe("ImageFile", () => {
117
117
  "test-file-id",
118
118
  readableFromBuffers(Buffer.from("test")),
119
119
  4,
120
- ),
120
+ ).whenUploaded(),
121
121
  ).rejects.toThrowError(
122
122
  "Failed to upload chunk 0 for /api/v1/image_files/test-file-id/upload 500 Internal Server Error",
123
123
  );
@@ -138,8 +138,11 @@ describe("ImageFile", () => {
138
138
  "test-file-id",
139
139
  readableFromBuffers(Buffer.from("test")),
140
140
  4,
141
- ),
142
- ).resolves.toBeUndefined();
141
+ ).whenUploaded(),
142
+ ).resolves.toEqual([
143
+ { type: "progress", progress: 0 },
144
+ { type: "progress", progress: 1 },
145
+ ]);
143
146
  });
144
147
  });
145
148
  });
@@ -1,7 +1,7 @@
1
1
  import type { Readable } from "node:stream";
2
2
 
3
- import { z } from "zod";
4
3
  import debug from "debug";
4
+ import { z } from "zod";
5
5
 
6
6
  import type { Client } from "../client.ts";
7
7
  import { uploadChunks } from "../uploadChunks.ts";
@@ -48,26 +48,18 @@ export const createImageFile = async (
48
48
  );
49
49
  };
50
50
 
51
- export const uploadImageFile = async (
51
+ export const uploadImageFile = (
52
52
  client: Client,
53
53
  fileId: string,
54
54
  fileStream: Readable,
55
55
  fileSize: number,
56
56
  ) => {
57
57
  log("Uploading image file", fileId);
58
- if (fileSize > MAX_IMAGE_SIZE) {
59
- throw new Error(
60
- `File size ${fileSize} bytes exceeds limit ${MAX_IMAGE_SIZE} bytes`,
61
- );
62
- }
63
58
 
64
- const result = await uploadChunks(client, {
59
+ return uploadChunks(client, {
65
60
  url: `/api/v1/image_files/${fileId}/upload`,
66
61
  fileSize,
67
62
  fileStream,
63
+ maxSize: MAX_IMAGE_SIZE,
68
64
  });
69
-
70
- log("Image file upload complete");
71
-
72
- return result;
73
65
  };
@@ -1,11 +1,12 @@
1
- import { test, expect, beforeAll, afterEach, afterAll, describe } from "vitest";
2
1
  import { http, HttpResponse } from "msw";
3
2
  import { setupServer } from "msw/node";
3
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
4
4
  import { ZodError } from "zod";
5
5
 
6
+ import { createTestTrack } from "TEST/createTestTrack.ts";
6
7
  import { Client } from "../client.ts";
7
- import { createISOBMFFTrack, uploadISOBMFFTrack } from "./isobmff-track.ts";
8
8
  import { readableFromBuffers } from "../readableFromBuffers.ts";
9
+ import { createISOBMFFTrack, uploadISOBMFFTrack } from "./isobmff-track.ts";
9
10
 
10
11
  const server = setupServer();
11
12
  const client = new Client("ef_TEST_TOKEN", "http://localhost");
@@ -93,7 +94,7 @@ describe("ISOBMFF Track", () => {
93
94
  1,
94
95
  readableFromBuffers(Buffer.from("test")),
95
96
  4,
96
- ),
97
+ ).whenUploaded(),
97
98
  ).rejects.toThrowError(
98
99
  "Failed to upload chunk 0 for /api/v1/isobmff_tracks/test-file/1/upload 500 Internal Server Error",
99
100
  );
@@ -115,46 +116,11 @@ describe("ISOBMFF Track", () => {
115
116
  1,
116
117
  readableFromBuffers(Buffer.from("test")),
117
118
  4,
118
- ),
119
- ).resolves.toBeUndefined();
119
+ ).whenUploaded(),
120
+ ).resolves.toEqual([
121
+ { type: "progress", progress: 0 },
122
+ { type: "progress", progress: 1 },
123
+ ]);
120
124
  });
121
125
  });
122
126
  });
123
-
124
- function createTestTrack(
125
- options: Partial<Parameters<typeof createISOBMFFTrack>[1]> = {},
126
- ) {
127
- return Object.assign(
128
- {
129
- file_id: "test-id",
130
- track_id: 1,
131
- type: "audio",
132
- probe_info: {
133
- channels: 2,
134
- sample_rate: "44100",
135
- duration: 1000,
136
- duration_ts: 1000,
137
- start_time: 0,
138
- start_pts: 0,
139
- r_frame_rate: "100",
140
- channel_layout: "stereo",
141
- codec_tag_string: "mp3",
142
- codec_long_name: "MP3",
143
- codec_type: "audio",
144
- codec_tag: "0x0000",
145
- codec_name: "aac",
146
- bits_per_sample: 16,
147
- index: 0,
148
- sample_fmt: "s16",
149
- time_base: "100",
150
- avg_frame_rate: "100",
151
- disposition: {},
152
- bit_rate: "100",
153
- },
154
- duration_ms: 1000,
155
- codec_name: "mp3",
156
- byte_size: 1024 * 1024 * 5,
157
- } as const,
158
- options,
159
- );
160
- }
@@ -63,7 +63,7 @@ export const createISOBMFFTrack = async (
63
63
  );
64
64
  };
65
65
 
66
- export const uploadISOBMFFTrack = async (
66
+ export const uploadISOBMFFTrack = (
67
67
  client: Client,
68
68
  fileId: string,
69
69
  trackId: number,
@@ -72,11 +72,10 @@ export const uploadISOBMFFTrack = async (
72
72
  ) => {
73
73
  log("Uploading fragment track", fileId);
74
74
 
75
- await uploadChunks(client, {
75
+ return uploadChunks(client, {
76
76
  url: `/api/v1/isobmff_tracks/${fileId}/${trackId}/upload`,
77
77
  fileStream,
78
78
  fileSize: trackSize,
79
+ maxSize: MAX_TRACK_SIZE,
79
80
  });
80
-
81
- log("Fragment track upload complete");
82
81
  };
@@ -1,11 +1,12 @@
1
1
  import { join } from "node:path";
2
2
 
3
- import { test, expect, beforeAll, afterEach, afterAll, describe } from "vitest";
4
3
  import { http, HttpResponse } from "msw";
5
4
  import { setupServer } from "msw/node";
5
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
6
6
  import { ZodError } from "zod";
7
7
 
8
8
  import { Client } from "../client.ts";
9
+ import { readableFromBuffers } from "../readableFromBuffers.ts";
9
10
  import {
10
11
  createUnprocessedFile,
11
12
  processAVFile,
@@ -15,7 +16,6 @@ import {
15
16
  updateUnprocessedFile,
16
17
  uploadUnprocessedFile,
17
18
  } from "./unprocessed-file.ts";
18
- import { readableFromBuffers } from "../readableFromBuffers.ts";
19
19
 
20
20
  const server = setupServer();
21
21
  const client = new Client("ef_TEST_TOKEN", "http://localhost");
@@ -147,7 +147,7 @@ describe("Unprocessed File", () => {
147
147
  "test-file",
148
148
  readableFromBuffers(Buffer.from("test")),
149
149
  4,
150
- ),
150
+ ).whenUploaded(),
151
151
  ).rejects.toThrowError(
152
152
  "Failed to upload chunk 0 for /api/v1/unprocessed_files/test-file/upload 500 Internal Server Error",
153
153
  );
@@ -168,8 +168,11 @@ describe("Unprocessed File", () => {
168
168
  "test-file",
169
169
  readableFromBuffers(Buffer.from("test")),
170
170
  4,
171
- ),
172
- ).resolves.toBeUndefined();
171
+ ).whenUploaded(),
172
+ ).resolves.toEqual([
173
+ { type: "progress", progress: 0 },
174
+ { type: "progress", progress: 1 },
175
+ ]);
173
176
  });
174
177
  });
175
178
 
@@ -183,7 +186,7 @@ describe("Unprocessed File", () => {
183
186
  );
184
187
 
185
188
  await expect(
186
- processAVFileBuffer(client, Buffer.from("test"), "test-file"),
189
+ processAVFileBuffer(client, Buffer.from("test"), "test-file").file(),
187
190
  ).rejects.toThrowError(
188
191
  "Failed to create unprocessed file 500 Internal Server Error",
189
192
  );
@@ -209,7 +212,7 @@ describe("Unprocessed File", () => {
209
212
  );
210
213
 
211
214
  await expect(
212
- processAVFileBuffer(client, Buffer.from("test"), "test-file"),
215
+ processAVFileBuffer(client, Buffer.from("test"), "test-file").file(),
213
216
  ).rejects.toThrowError(
214
217
  "Failed to upload chunk 0 for /api/v1/unprocessed_files/098f6bcd-4621-d373-cade-4e832627b4f6/upload 500 Internal Server Error",
215
218
  );
@@ -239,7 +242,7 @@ describe("Unprocessed File", () => {
239
242
  );
240
243
 
241
244
  await expect(
242
- processAVFileBuffer(client, Buffer.from("test"), "test-file"),
245
+ processAVFileBuffer(client, Buffer.from("test"), "test-file").file(),
243
246
  ).rejects.toThrowError(
244
247
  "Failed to update unprocessed file 500 Internal Server Error",
245
248
  );
@@ -269,7 +272,7 @@ describe("Unprocessed File", () => {
269
272
  );
270
273
 
271
274
  await expect(
272
- processAVFileBuffer(client, Buffer.from("test"), "test-file"),
275
+ processAVFileBuffer(client, Buffer.from("test"), "test-file").file(),
273
276
  ).resolves.toEqual({ test: "response" });
274
277
  });
275
278
  });
@@ -282,7 +285,9 @@ describe("Unprocessed File", () => {
282
285
  ),
283
286
  );
284
287
 
285
- await expect(processAVFile(client, TEST_AV_FILE)).rejects.toThrowError(
288
+ await expect(
289
+ processAVFile(client, TEST_AV_FILE).file(),
290
+ ).rejects.toThrowError(
286
291
  "Failed to create unprocessed file 500 Internal Server Error",
287
292
  );
288
293
  });
@@ -306,7 +311,9 @@ describe("Unprocessed File", () => {
306
311
  ),
307
312
  );
308
313
 
309
- await expect(processAVFile(client, TEST_AV_FILE)).rejects.toThrowError(
314
+ await expect(
315
+ processAVFile(client, TEST_AV_FILE).file(),
316
+ ).rejects.toThrowError(
310
317
  "Failed to upload chunk 0 for /api/v1/unprocessed_files/098f6bcd-4621-d373-cade-4e832627b4f6/upload 500 Internal Server Error",
311
318
  );
312
319
  });
@@ -334,7 +341,9 @@ describe("Unprocessed File", () => {
334
341
  ),
335
342
  );
336
343
 
337
- await expect(processAVFile(client, TEST_AV_FILE)).rejects.toThrowError(
344
+ await expect(
345
+ processAVFile(client, TEST_AV_FILE).file(),
346
+ ).rejects.toThrowError(
338
347
  "Failed to update unprocessed file 500 Internal Server Error",
339
348
  );
340
349
  });
@@ -362,9 +371,11 @@ describe("Unprocessed File", () => {
362
371
  ),
363
372
  );
364
373
 
365
- await expect(processAVFile(client, TEST_AV_FILE)).resolves.toEqual({
366
- test: "response",
367
- });
374
+ await expect(processAVFile(client, TEST_AV_FILE).file()).resolves.toEqual(
375
+ {
376
+ test: "response",
377
+ },
378
+ );
368
379
  });
369
380
  });
370
381
 
@@ -377,7 +388,7 @@ describe("Unprocessed File", () => {
377
388
  );
378
389
 
379
390
  await expect(
380
- processImageFileBuffer(client, Buffer.from("test"), "test-file"),
391
+ processImageFileBuffer(client, Buffer.from("test"), "test-file").file(),
381
392
  ).rejects.toThrowError(
382
393
  "Failed to create unprocessed file 500 Internal Server Error",
383
394
  );
@@ -403,7 +414,7 @@ describe("Unprocessed File", () => {
403
414
  );
404
415
 
405
416
  await expect(
406
- processImageFileBuffer(client, Buffer.from("test"), "test-file"),
417
+ processImageFileBuffer(client, Buffer.from("test"), "test-file").file(),
407
418
  ).rejects.toThrowError(
408
419
  "Failed to upload chunk 0 for /api/v1/unprocessed_files/098f6bcd-4621-d373-cade-4e832627b4f6/upload 500 Internal Server Error",
409
420
  );
@@ -433,7 +444,7 @@ describe("Unprocessed File", () => {
433
444
  );
434
445
 
435
446
  await expect(
436
- processImageFileBuffer(client, Buffer.from("test"), "test-file"),
447
+ processImageFileBuffer(client, Buffer.from("test"), "test-file").file(),
437
448
  ).rejects.toThrowError(
438
449
  "Failed to update unprocessed file 500 Internal Server Error",
439
450
  );
@@ -463,7 +474,7 @@ describe("Unprocessed File", () => {
463
474
  );
464
475
 
465
476
  await expect(
466
- processImageFileBuffer(client, Buffer.from("test"), "test-file"),
477
+ processImageFileBuffer(client, Buffer.from("test"), "test-file").file(),
467
478
  ).resolves.toEqual({ test: "response" });
468
479
  });
469
480
  });
@@ -476,7 +487,9 @@ describe("Unprocessed File", () => {
476
487
  ),
477
488
  );
478
489
 
479
- await expect(processImageFile(client, TEST_AV_FILE)).rejects.toThrowError(
490
+ await expect(
491
+ processImageFile(client, TEST_AV_FILE).file(),
492
+ ).rejects.toThrowError(
480
493
  "Failed to create unprocessed file 500 Internal Server Error",
481
494
  );
482
495
  });
@@ -500,7 +513,9 @@ describe("Unprocessed File", () => {
500
513
  ),
501
514
  );
502
515
 
503
- await expect(processImageFile(client, TEST_AV_FILE)).rejects.toThrowError(
516
+ await expect(
517
+ processImageFile(client, TEST_AV_FILE).file(),
518
+ ).rejects.toThrowError(
504
519
  "Failed to upload chunk 0 for /api/v1/unprocessed_files/098f6bcd-4621-d373-cade-4e832627b4f6/upload 500 Internal Server Error",
505
520
  );
506
521
  });
@@ -528,7 +543,9 @@ describe("Unprocessed File", () => {
528
543
  ),
529
544
  );
530
545
 
531
- await expect(processImageFile(client, TEST_AV_FILE)).rejects.toThrowError(
546
+ await expect(
547
+ processImageFile(client, TEST_AV_FILE).file(),
548
+ ).rejects.toThrowError(
532
549
  "Failed to update unprocessed file 500 Internal Server Error",
533
550
  );
534
551
  });
@@ -555,6 +572,12 @@ describe("Unprocessed File", () => {
555
572
  () => HttpResponse.json({ test: "response" }, { status: 200 }),
556
573
  ),
557
574
  );
575
+
576
+ await expect(
577
+ processImageFile(client, TEST_AV_FILE).file(),
578
+ ).resolves.toEqual({
579
+ test: "response",
580
+ });
558
581
  });
559
582
  });
560
583
  });
@@ -9,7 +9,11 @@ import { z } from "zod";
9
9
  import { md5Buffer, md5FilePath } from "@editframe/assets";
10
10
 
11
11
  import type { Client } from "../client.ts";
12
- import { uploadChunks } from "../uploadChunks.ts";
12
+ import {
13
+ type IteratorWithPromise,
14
+ type UploadChunkEvent,
15
+ uploadChunks,
16
+ } from "../uploadChunks.ts";
13
17
 
14
18
  const log = debug("ef:api:unprocessed-file");
15
19
 
@@ -118,7 +122,7 @@ export const updateUnprocessedFile = async (
118
122
  );
119
123
  };
120
124
 
121
- export const uploadUnprocessedFile = async (
125
+ export const uploadUnprocessedFile = (
122
126
  client: Client,
123
127
  fileId: string,
124
128
  fileStream: Readable,
@@ -126,64 +130,71 @@ export const uploadUnprocessedFile = async (
126
130
  ) => {
127
131
  log("Uploading unprocessed file", fileId);
128
132
 
129
- await uploadChunks(client, {
133
+ return uploadChunks(client, {
130
134
  url: `/api/v1/unprocessed_files/${fileId}/upload`,
131
135
  fileSize,
132
136
  fileStream,
137
+ maxSize: MAX_FILE_SIZE,
133
138
  });
134
-
135
- log("Unprocessed file upload complete");
136
139
  };
137
140
 
138
- const processResource = async (
141
+ const processResource = (
139
142
  client: Client,
140
143
  filename: string,
141
144
  md5: string,
142
145
  byteSize: number,
143
146
  processor: z.infer<typeof FileProcessor>,
144
- doUpload: (id: string) => Promise<void>,
147
+ doUpload: (id: string) => IteratorWithPromise<UploadChunkEvent>,
145
148
  ) => {
146
149
  log("Processing", { filename, md5, byteSize, processor });
147
150
 
148
- const unprocessedFile = await createUnprocessedFile(client, {
151
+ const createFilePromise = createUnprocessedFile(client, {
149
152
  md5: md5,
150
153
  processes: [],
151
154
  filename,
152
155
  byte_size: byteSize,
153
156
  });
154
157
 
155
- if (unprocessedFile.complete === false) {
156
- await doUpload(unprocessedFile.id);
157
- }
158
-
159
- if (unprocessedFile.processes.includes(processor)) {
160
- log("File already processed", unprocessedFile);
161
- return unprocessedFile;
162
- }
163
-
164
- const fileInformation = await updateUnprocessedFile(
165
- client,
166
- unprocessedFile.id,
167
- {
168
- processes: [processor],
158
+ return {
159
+ async *progress() {
160
+ const unprocessedFile = await createFilePromise;
161
+ if (unprocessedFile.complete) {
162
+ yield { type: "progress", progress: 0 };
163
+ yield { type: "progress", progress: 1 };
164
+ }
165
+ yield* doUpload(unprocessedFile.id);
169
166
  },
170
- );
171
- log("File processed", fileInformation);
172
- return fileInformation;
167
+ file: async () => {
168
+ const unprocessedFile = await createFilePromise;
169
+ if (unprocessedFile.complete) {
170
+ return unprocessedFile;
171
+ }
172
+ await doUpload(unprocessedFile.id).whenUploaded();
173
+ const fileInformation = await updateUnprocessedFile(
174
+ client,
175
+ unprocessedFile.id,
176
+ {
177
+ processes: [processor],
178
+ },
179
+ );
180
+ log("File processed", fileInformation);
181
+ return fileInformation;
182
+ },
183
+ };
173
184
  };
174
185
 
175
186
  const buildBufferProcessor = (processor: z.infer<typeof FileProcessor>) => {
176
- return async (client: Client, buffer: Buffer, filename = "buffer") => {
187
+ return (client: Client, buffer: Buffer, filename = "buffer") => {
177
188
  log(`Processing file buffer: ${processor}`, filename);
178
189
  const md5 = md5Buffer(buffer);
179
190
 
180
- return await processResource(
191
+ return processResource(
181
192
  client,
182
193
  filename,
183
194
  md5,
184
195
  buffer.byteLength,
185
196
  processor,
186
- async (id: string) => {
197
+ (id: string) => {
187
198
  const readStream = new Readable({
188
199
  read() {
189
200
  readStream.push(buffer);
@@ -191,29 +202,35 @@ const buildBufferProcessor = (processor: z.infer<typeof FileProcessor>) => {
191
202
  },
192
203
  });
193
204
 
194
- await uploadUnprocessedFile(client, id, readStream, buffer.byteLength);
205
+ return uploadUnprocessedFile(client, id, readStream, buffer.byteLength);
195
206
  },
196
207
  );
197
208
  };
198
209
  };
199
210
 
200
211
  const buildFileProcessor = (processor: z.infer<typeof FileProcessor>) => {
201
- return async (client: Client, filePath: string) => {
202
- log(`Processing file ${processor}`, filePath);
203
- const md5 = await md5FilePath(filePath);
204
- const byteSize = (await stat(filePath)).size;
205
-
206
- return await processResource(
207
- client,
208
- basename(filePath),
209
- md5,
210
- byteSize,
211
- processor,
212
- async (id: string) => {
213
- const readStream = createReadStream(filePath);
214
- return await uploadUnprocessedFile(client, id, readStream, byteSize);
215
- },
216
- );
212
+ return (client: Client, filePath: string) => {
213
+ const processPromise = async () => {
214
+ const [md5, { size: byteSize }] = await Promise.all([
215
+ md5FilePath(filePath),
216
+ stat(filePath),
217
+ ]);
218
+ return processResource(
219
+ client,
220
+ basename(filePath),
221
+ md5,
222
+ byteSize,
223
+ processor,
224
+ (id: string) => {
225
+ const readStream = createReadStream(filePath);
226
+ return uploadUnprocessedFile(client, id, readStream, byteSize);
227
+ },
228
+ );
229
+ };
230
+ return {
231
+ progress: async () => (await processPromise()).progress(),
232
+ file: async () => (await processPromise()).file(),
233
+ };
217
234
  };
218
235
  };
219
236