@editframe/api 0.10.0-beta.6 → 0.11.0-beta.1

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,29 +1,30 @@
1
1
  import { Readable } from 'node:stream';
2
2
  import { z } from 'zod';
3
3
  import { Client } from '../client.ts';
4
- declare const FileProcessors: z.ZodEffects<z.ZodArray<z.ZodUnion<[z.ZodLiteral<"isobmff">, z.ZodLiteral<"image">, z.ZodLiteral<"captions">]>, "many">, ("captions" | "image" | "isobmff")[], ("captions" | "image" | "isobmff")[]>;
4
+ import { IteratorWithPromise, UploadChunkEvent } from '../uploadChunks.ts';
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;
7
8
  filename: z.ZodString;
8
- processes: z.ZodOptional<z.ZodEffects<z.ZodArray<z.ZodUnion<[z.ZodLiteral<"isobmff">, z.ZodLiteral<"image">, z.ZodLiteral<"captions">]>, "many">, ("captions" | "image" | "isobmff")[], ("captions" | "image" | "isobmff")[]>>;
9
+ processes: z.ZodOptional<z.ZodEffects<z.ZodArray<z.ZodUnion<[z.ZodLiteral<"isobmff">, z.ZodLiteral<"image">, z.ZodLiteral<"captions">, z.ZodString]>, "many">, string[], string[]>>;
9
10
  byte_size: z.ZodNumber;
10
11
  }, "strip", z.ZodTypeAny, {
11
12
  md5: string;
12
13
  filename: string;
13
14
  byte_size: number;
14
- processes?: ("captions" | "image" | "isobmff")[] | undefined;
15
+ processes?: string[] | undefined;
15
16
  }, {
16
17
  md5: string;
17
18
  filename: string;
18
19
  byte_size: number;
19
- processes?: ("captions" | "image" | "isobmff")[] | undefined;
20
+ processes?: string[] | undefined;
20
21
  }>;
21
22
  export declare const UpdateUnprocessedFilePayload: z.ZodObject<{
22
- processes: z.ZodOptional<z.ZodEffects<z.ZodArray<z.ZodUnion<[z.ZodLiteral<"isobmff">, z.ZodLiteral<"image">, z.ZodLiteral<"captions">]>, "many">, ("captions" | "image" | "isobmff")[], ("captions" | "image" | "isobmff")[]>>;
23
+ processes: z.ZodOptional<z.ZodEffects<z.ZodArray<z.ZodUnion<[z.ZodLiteral<"isobmff">, z.ZodLiteral<"image">, z.ZodLiteral<"captions">, z.ZodString]>, "many">, string[], string[]>>;
23
24
  }, "strip", z.ZodTypeAny, {
24
- processes?: ("captions" | "image" | "isobmff")[] | undefined;
25
+ processes?: string[] | undefined;
25
26
  }, {
26
- processes?: ("captions" | "image" | "isobmff")[] | undefined;
27
+ processes?: string[] | undefined;
27
28
  }>;
28
29
  export interface CreateUnprocessedFileResult {
29
30
  byte_size: number;
@@ -31,7 +32,7 @@ export interface CreateUnprocessedFileResult {
31
32
  complete: boolean;
32
33
  id: string;
33
34
  md5: string;
34
- processes: z.infer<typeof FileProcessors>;
35
+ processes: z.infer<typeof FileProcessors> & string[];
35
36
  asset_id: string;
36
37
  }
37
38
  export interface UpdateUnprocessedFileResult {
@@ -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 {};
@@ -1,16 +1,17 @@
1
- import { Readable } from "node:stream";
2
- import { basename } from "node:path";
3
1
  import { createReadStream } from "node:fs";
4
2
  import { stat } from "node:fs/promises";
5
- import { z } from "zod";
3
+ import { basename } from "node:path";
4
+ import { Readable } from "node:stream";
6
5
  import debug from "debug";
6
+ import { z } from "zod";
7
7
  import { md5Buffer, md5FilePath } from "@editframe/assets";
8
8
  import { uploadChunks } from "../uploadChunks.js";
9
9
  const log = debug("ef:api:unprocessed-file");
10
10
  const FileProcessor = z.union([
11
11
  z.literal("isobmff"),
12
12
  z.literal("image"),
13
- z.literal("captions")
13
+ z.literal("captions"),
14
+ z.string()
14
15
  ]);
15
16
  const FileProcessors = z.array(FileProcessor).refine(
16
17
  (value) => {
@@ -71,78 +72,95 @@ const updateUnprocessedFile = async (client, fileId, payload) => {
71
72
  `Failed to update unprocessed file ${response.status} ${response.statusText}`
72
73
  );
73
74
  };
74
- const uploadUnprocessedFile = async (client, fileId, fileStream, fileSize) => {
75
+ const uploadUnprocessedFile = (client, fileId, fileStream, fileSize) => {
75
76
  log("Uploading unprocessed file", fileId);
76
- await uploadChunks(client, {
77
+ return uploadChunks(client, {
77
78
  url: `/api/v1/unprocessed_files/${fileId}/upload`,
78
79
  fileSize,
79
- fileStream
80
+ fileStream,
81
+ maxSize: MAX_FILE_SIZE
80
82
  });
81
- log("Unprocessed file upload complete");
82
83
  };
83
- const processResource = async (client, filename, md5, byteSize, processor, doUpload) => {
84
+ const processResource = (client, filename, md5, byteSize, processor, doUpload) => {
84
85
  log("Processing", { filename, md5, byteSize, processor });
85
- const unprocessedFile = await createUnprocessedFile(client, {
86
+ const createFilePromise = createUnprocessedFile(client, {
86
87
  md5,
87
88
  processes: [],
88
89
  filename,
89
90
  byte_size: byteSize
90
91
  });
91
- if (unprocessedFile.complete === false) {
92
- await doUpload(unprocessedFile.id);
93
- }
94
- if (unprocessedFile.processes.includes(processor)) {
95
- log("File already processed", unprocessedFile);
96
- return unprocessedFile;
97
- }
98
- const fileInformation = await updateUnprocessedFile(
99
- client,
100
- unprocessedFile.id,
101
- {
102
- 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;
103
116
  }
104
- );
105
- log("File processed", fileInformation);
106
- return fileInformation;
117
+ };
107
118
  };
108
119
  const buildBufferProcessor = (processor) => {
109
- return async (client, buffer, filename = "buffer") => {
120
+ return (client, buffer, filename = "buffer") => {
110
121
  log(`Processing file buffer: ${processor}`, filename);
111
122
  const md5 = md5Buffer(buffer);
112
- return await processResource(
123
+ return processResource(
113
124
  client,
114
125
  filename,
115
126
  md5,
116
127
  buffer.byteLength,
117
128
  processor,
118
- async (id) => {
129
+ (id) => {
119
130
  const readStream = new Readable({
120
131
  read() {
121
132
  readStream.push(buffer);
122
133
  readStream.push(null);
123
134
  }
124
135
  });
125
- await uploadUnprocessedFile(client, id, readStream, buffer.byteLength);
136
+ return uploadUnprocessedFile(client, id, readStream, buffer.byteLength);
126
137
  }
127
138
  );
128
139
  };
129
140
  };
130
141
  const buildFileProcessor = (processor) => {
131
- return async (client, filePath) => {
132
- log(`Processing file ${processor}`, filePath);
133
- const md5 = await md5FilePath(filePath);
134
- const byteSize = (await stat(filePath)).size;
135
- return await processResource(
136
- client,
137
- basename(filePath),
138
- md5,
139
- byteSize,
140
- processor,
141
- async (id) => {
142
- const readStream = createReadStream(filePath);
143
- return await uploadUnprocessedFile(client, id, readStream, byteSize);
144
- }
145
- );
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
+ };
146
164
  };
147
165
  };
148
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.6",
3
+ "version": "0.11.0-beta.1",
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.6",
31
+ "@editframe/assets": "0.11.0-beta.1",
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,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 { createISOBMFFTrack, uploadISOBMFFTrack } from "./isobmff-track.ts";
8
7
  import { readableFromBuffers } from "../readableFromBuffers.ts";
8
+ import { createISOBMFFTrack, uploadISOBMFFTrack } from "./isobmff-track.ts";
9
9
 
10
10
  const server = setupServer();
11
11
  const client = new Client("ef_TEST_TOKEN", "http://localhost");
@@ -93,7 +93,7 @@ describe("ISOBMFF Track", () => {
93
93
  1,
94
94
  readableFromBuffers(Buffer.from("test")),
95
95
  4,
96
- ),
96
+ ).whenUploaded(),
97
97
  ).rejects.toThrowError(
98
98
  "Failed to upload chunk 0 for /api/v1/isobmff_tracks/test-file/1/upload 500 Internal Server Error",
99
99
  );
@@ -115,8 +115,11 @@ describe("ISOBMFF Track", () => {
115
115
  1,
116
116
  readableFromBuffers(Buffer.from("test")),
117
117
  4,
118
- ),
119
- ).resolves.toBeUndefined();
118
+ ).whenUploaded(),
119
+ ).resolves.toEqual([
120
+ { type: "progress", progress: 0 },
121
+ { type: "progress", progress: 1 },
122
+ ]);
120
123
  });
121
124
  });
122
125
  });
@@ -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
  });
@@ -1,15 +1,19 @@
1
- import { Readable } from "node:stream";
2
- import { basename } from "node:path";
3
1
  import { createReadStream } from "node:fs";
4
2
  import { stat } from "node:fs/promises";
3
+ import { basename } from "node:path";
4
+ import { Readable } from "node:stream";
5
5
 
6
- import { z } from "zod";
7
6
  import debug from "debug";
7
+ import { z } from "zod";
8
8
 
9
- import { md5FilePath, md5Buffer } from "@editframe/assets";
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
 
@@ -17,6 +21,7 @@ const FileProcessor = z.union([
17
21
  z.literal("isobmff"),
18
22
  z.literal("image"),
19
23
  z.literal("captions"),
24
+ z.string(),
20
25
  ]);
21
26
 
22
27
  const FileProcessors = z.array(FileProcessor).refine(
@@ -47,7 +52,7 @@ export interface CreateUnprocessedFileResult {
47
52
  complete: boolean;
48
53
  id: string;
49
54
  md5: string;
50
- processes: z.infer<typeof FileProcessors>;
55
+ processes: z.infer<typeof FileProcessors> & string[];
51
56
  asset_id: string;
52
57
  }
53
58
 
@@ -117,7 +122,7 @@ export const updateUnprocessedFile = async (
117
122
  );
118
123
  };
119
124
 
120
- export const uploadUnprocessedFile = async (
125
+ export const uploadUnprocessedFile = (
121
126
  client: Client,
122
127
  fileId: string,
123
128
  fileStream: Readable,
@@ -125,64 +130,71 @@ export const uploadUnprocessedFile = async (
125
130
  ) => {
126
131
  log("Uploading unprocessed file", fileId);
127
132
 
128
- await uploadChunks(client, {
133
+ return uploadChunks(client, {
129
134
  url: `/api/v1/unprocessed_files/${fileId}/upload`,
130
135
  fileSize,
131
136
  fileStream,
137
+ maxSize: MAX_FILE_SIZE,
132
138
  });
133
-
134
- log("Unprocessed file upload complete");
135
139
  };
136
140
 
137
- const processResource = async (
141
+ const processResource = (
138
142
  client: Client,
139
143
  filename: string,
140
144
  md5: string,
141
145
  byteSize: number,
142
146
  processor: z.infer<typeof FileProcessor>,
143
- doUpload: (id: string) => Promise<void>,
147
+ doUpload: (id: string) => IteratorWithPromise<UploadChunkEvent>,
144
148
  ) => {
145
149
  log("Processing", { filename, md5, byteSize, processor });
146
150
 
147
- const unprocessedFile = await createUnprocessedFile(client, {
151
+ const createFilePromise = createUnprocessedFile(client, {
148
152
  md5: md5,
149
153
  processes: [],
150
154
  filename,
151
155
  byte_size: byteSize,
152
156
  });
153
157
 
154
- if (unprocessedFile.complete === false) {
155
- await doUpload(unprocessedFile.id);
156
- }
157
-
158
- if (unprocessedFile.processes.includes(processor)) {
159
- log("File already processed", unprocessedFile);
160
- return unprocessedFile;
161
- }
162
-
163
- const fileInformation = await updateUnprocessedFile(
164
- client,
165
- unprocessedFile.id,
166
- {
167
- 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);
168
166
  },
169
- );
170
- log("File processed", fileInformation);
171
- 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
+ };
172
184
  };
173
185
 
174
186
  const buildBufferProcessor = (processor: z.infer<typeof FileProcessor>) => {
175
- return async (client: Client, buffer: Buffer, filename = "buffer") => {
187
+ return (client: Client, buffer: Buffer, filename = "buffer") => {
176
188
  log(`Processing file buffer: ${processor}`, filename);
177
189
  const md5 = md5Buffer(buffer);
178
190
 
179
- return await processResource(
191
+ return processResource(
180
192
  client,
181
193
  filename,
182
194
  md5,
183
195
  buffer.byteLength,
184
196
  processor,
185
- async (id: string) => {
197
+ (id: string) => {
186
198
  const readStream = new Readable({
187
199
  read() {
188
200
  readStream.push(buffer);
@@ -190,29 +202,35 @@ const buildBufferProcessor = (processor: z.infer<typeof FileProcessor>) => {
190
202
  },
191
203
  });
192
204
 
193
- await uploadUnprocessedFile(client, id, readStream, buffer.byteLength);
205
+ return uploadUnprocessedFile(client, id, readStream, buffer.byteLength);
194
206
  },
195
207
  );
196
208
  };
197
209
  };
198
210
 
199
211
  const buildFileProcessor = (processor: z.infer<typeof FileProcessor>) => {
200
- return async (client: Client, filePath: string) => {
201
- log(`Processing file ${processor}`, filePath);
202
- const md5 = await md5FilePath(filePath);
203
- const byteSize = (await stat(filePath)).size;
204
-
205
- return await processResource(
206
- client,
207
- basename(filePath),
208
- md5,
209
- byteSize,
210
- processor,
211
- async (id: string) => {
212
- const readStream = createReadStream(filePath);
213
- return await uploadUnprocessedFile(client, id, readStream, byteSize);
214
- },
215
- );
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
+ };
216
234
  };
217
235
  };
218
236