@editframe/cli 0.10.0-beta.4 → 0.10.0-beta.6
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/dist/VERSION.d.ts +1 -1
- package/dist/VERSION.js +1 -1
- package/dist/commands/render.d.ts +1 -1
- package/dist/commands/render.js +77 -51
- package/dist/commands/sync.js +5 -2
- package/dist/operations/getRenderInfo.d.ts +40 -4
- package/dist/operations/getRenderInfo.js +13 -0
- package/dist/operations/processRenderInfo.js +3 -0
- package/dist/operations/syncAssetsDirectory/SubAssetSync.d.ts +20 -0
- package/dist/operations/syncAssetsDirectory/SubAssetSync.js +26 -0
- package/dist/operations/syncAssetsDirectory/SyncCaption.d.ts +19 -0
- package/dist/operations/syncAssetsDirectory/SyncCaption.js +66 -0
- package/dist/operations/syncAssetsDirectory/SyncCaption.test.d.ts +1 -0
- package/dist/operations/syncAssetsDirectory/SyncFragmentIndex.d.ts +20 -0
- package/dist/operations/syncAssetsDirectory/SyncFragmentIndex.js +79 -0
- package/dist/operations/syncAssetsDirectory/SyncFragmentIndex.test.d.ts +1 -0
- package/dist/operations/syncAssetsDirectory/SyncImage.d.ts +23 -0
- package/dist/operations/syncAssetsDirectory/SyncImage.js +95 -0
- package/dist/operations/syncAssetsDirectory/SyncImage.test.d.ts +1 -0
- package/dist/operations/syncAssetsDirectory/SyncStatus.d.ts +41 -0
- package/dist/operations/syncAssetsDirectory/SyncStatus.js +43 -0
- package/dist/operations/syncAssetsDirectory/SyncTrack.d.ts +70 -0
- package/dist/operations/syncAssetsDirectory/SyncTrack.js +138 -0
- package/dist/operations/syncAssetsDirectory/SyncTrack.test.d.ts +1 -0
- package/dist/operations/syncAssetsDirectory/doAssetSync.d.ts +5 -0
- package/dist/operations/syncAssetsDirectory/doAssetSync.js +48 -0
- package/dist/operations/syncAssetsDirectory/doAssetSync.test.d.ts +1 -0
- package/dist/operations/syncAssetsDirectory.d.ts +1 -1
- package/dist/operations/syncAssetsDirectory.js +20 -240
- package/dist/test-fixtures/fixture.d.ts +26 -0
- package/dist/utils/index.js +4 -1
- package/package.json +5 -5
- package/src/commands/process.ts +0 -1
- package/src/commands/render.ts +79 -58
- package/src/commands/sync.ts +5 -2
- package/src/operations/getRenderInfo.ts +14 -0
- package/src/operations/processRenderInfo.ts +3 -0
- package/src/operations/syncAssetsDirectory/SubAssetSync.ts +42 -0
- package/src/operations/syncAssetsDirectory/SyncCaption.test.ts +145 -0
- package/src/operations/syncAssetsDirectory/SyncCaption.ts +76 -0
- package/src/operations/syncAssetsDirectory/SyncFragmentIndex.test.ts +151 -0
- package/src/operations/syncAssetsDirectory/SyncFragmentIndex.ts +92 -0
- package/src/operations/syncAssetsDirectory/SyncImage.test.ts +131 -0
- package/src/operations/syncAssetsDirectory/SyncImage.ts +112 -0
- package/src/operations/syncAssetsDirectory/SyncStatus.ts +51 -0
- package/src/operations/syncAssetsDirectory/SyncTrack.test.ts +222 -0
- package/src/operations/syncAssetsDirectory/SyncTrack.ts +164 -0
- package/src/operations/syncAssetsDirectory/doAssetSync.test.ts +134 -0
- package/src/operations/syncAssetsDirectory/doAssetSync.ts +62 -0
- package/src/operations/syncAssetsDirectory.test.ts +482 -0
- package/src/operations/syncAssetsDirectory.ts +22 -283
- package/src/utils/index.ts +4 -1
- package/test-fixtures/fixture.ts +141 -0
- package/test-fixtures/network.ts +181 -0
- package/test-fixtures/test-captions.json +9 -0
- package/test-fixtures/test.mp4 +0 -0
- package/test-fixtures/test.png +0 -0
- package/src/commands/render.test.ts +0 -34
- /package/dist/{commands/render.test.d.ts → operations/syncAssetsDirectory.test.d.ts} +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { fixture, withFixtures } from "../../../test-fixtures/fixture.ts";
|
|
3
|
+
import {
|
|
4
|
+
mockCreateIsobmffFile,
|
|
5
|
+
mockUploadIsobmffFileIndex,
|
|
6
|
+
useMSW,
|
|
7
|
+
} from "../../../test-fixtures/network.ts";
|
|
8
|
+
import { SyncFragmentIndex } from "./SyncFragmentIndex.ts";
|
|
9
|
+
|
|
10
|
+
describe("SyncFragmentIndex", async () => {
|
|
11
|
+
const server = useMSW();
|
|
12
|
+
await withFixtures(
|
|
13
|
+
[fixture("test.mp4", "test.mp4")],
|
|
14
|
+
async ({ files: [video], generateTrackFragmentIndex }) => {
|
|
15
|
+
test("Reads byte size", async () => {
|
|
16
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
17
|
+
await generateTrackFragmentIndex(video!),
|
|
18
|
+
video!.md5,
|
|
19
|
+
);
|
|
20
|
+
await expect(syncFragmentIndex.byteSize()).resolves.toEqual(31);
|
|
21
|
+
});
|
|
22
|
+
test("prepare() is noop", async () => {
|
|
23
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
24
|
+
await generateTrackFragmentIndex(video!),
|
|
25
|
+
video!.md5,
|
|
26
|
+
);
|
|
27
|
+
await expect(syncFragmentIndex.prepare()).resolves.toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
test("validate() is noop", async () => {
|
|
30
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
31
|
+
await generateTrackFragmentIndex(video!),
|
|
32
|
+
video!.md5,
|
|
33
|
+
);
|
|
34
|
+
await expect(syncFragmentIndex.validate()).resolves.toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
describe(".create()", () => {
|
|
37
|
+
test("isComplete() returns false when not created", async () => {
|
|
38
|
+
server.use(
|
|
39
|
+
mockCreateIsobmffFile({
|
|
40
|
+
complete: false,
|
|
41
|
+
id: "123",
|
|
42
|
+
filename: "test.mp4",
|
|
43
|
+
fixture: video!,
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
47
|
+
await generateTrackFragmentIndex(video!),
|
|
48
|
+
video!.md5,
|
|
49
|
+
);
|
|
50
|
+
await syncFragmentIndex.create();
|
|
51
|
+
expect(syncFragmentIndex.isComplete()).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
test("isComplete() returns true when created", async () => {
|
|
54
|
+
server.use(
|
|
55
|
+
mockCreateIsobmffFile({
|
|
56
|
+
complete: true,
|
|
57
|
+
id: "123",
|
|
58
|
+
filename: "test.mp4",
|
|
59
|
+
fixture: video!,
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
63
|
+
await generateTrackFragmentIndex(video!),
|
|
64
|
+
video!.md5,
|
|
65
|
+
);
|
|
66
|
+
await syncFragmentIndex.create();
|
|
67
|
+
expect(syncFragmentIndex.isComplete()).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe(".upload()", () => {
|
|
71
|
+
test("throws when not created", async () => {
|
|
72
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
73
|
+
await generateTrackFragmentIndex(video!),
|
|
74
|
+
video!.md5,
|
|
75
|
+
);
|
|
76
|
+
await expect(syncFragmentIndex.upload()).rejects.toThrow();
|
|
77
|
+
});
|
|
78
|
+
test("uploads caption", async () => {
|
|
79
|
+
server.use(
|
|
80
|
+
mockCreateIsobmffFile({
|
|
81
|
+
complete: true,
|
|
82
|
+
id: "123",
|
|
83
|
+
filename: "test.mp4",
|
|
84
|
+
fixture: video!,
|
|
85
|
+
}),
|
|
86
|
+
mockUploadIsobmffFileIndex({
|
|
87
|
+
id: "123",
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
91
|
+
await generateTrackFragmentIndex(video!),
|
|
92
|
+
video!.md5,
|
|
93
|
+
);
|
|
94
|
+
await syncFragmentIndex.create();
|
|
95
|
+
await expect(syncFragmentIndex.upload()).resolves.toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe(".markSynced()", () => {
|
|
99
|
+
test("throws when not created", async () => {
|
|
100
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
101
|
+
await generateTrackFragmentIndex(video!),
|
|
102
|
+
video!.md5,
|
|
103
|
+
);
|
|
104
|
+
await expect(syncFragmentIndex.markSynced()).rejects.toThrow();
|
|
105
|
+
});
|
|
106
|
+
test("marks synced", async () => {
|
|
107
|
+
server.use(
|
|
108
|
+
mockCreateIsobmffFile({
|
|
109
|
+
complete: true,
|
|
110
|
+
id: "123",
|
|
111
|
+
fixture: video!,
|
|
112
|
+
}),
|
|
113
|
+
mockUploadIsobmffFileIndex({
|
|
114
|
+
id: "123",
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
const syncFragmentIndex = new SyncFragmentIndex(
|
|
118
|
+
await generateTrackFragmentIndex(video!),
|
|
119
|
+
video!.md5,
|
|
120
|
+
);
|
|
121
|
+
await syncFragmentIndex.create();
|
|
122
|
+
await syncFragmentIndex.markSynced();
|
|
123
|
+
await expect(syncFragmentIndex.syncStatus.isSynced()).resolves.toBe(
|
|
124
|
+
true,
|
|
125
|
+
);
|
|
126
|
+
await expect(
|
|
127
|
+
syncFragmentIndex.syncStatus.readInfo(),
|
|
128
|
+
).resolves.toEqual({
|
|
129
|
+
version: "1",
|
|
130
|
+
complete: true,
|
|
131
|
+
id: "123",
|
|
132
|
+
md5: video!.md5,
|
|
133
|
+
asset_id: `${video!.md5}:test.mp4`,
|
|
134
|
+
byte_size: 31,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await expect(
|
|
138
|
+
syncFragmentIndex.fileSyncStatus.readInfo(),
|
|
139
|
+
).resolves.toEqual({
|
|
140
|
+
version: "1",
|
|
141
|
+
complete: true,
|
|
142
|
+
id: "123",
|
|
143
|
+
md5: video!.md5,
|
|
144
|
+
asset_id: `${video!.md5}:test.mp4`,
|
|
145
|
+
byte_size: 31,
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type CreateISOBMFFFileResult,
|
|
7
|
+
createISOBMFFFile,
|
|
8
|
+
uploadFragmentIndex,
|
|
9
|
+
} from "@editframe/api";
|
|
10
|
+
|
|
11
|
+
import { getClient } from "../../utils/index.ts";
|
|
12
|
+
import type { SubAssetSync } from "./SubAssetSync.ts";
|
|
13
|
+
import { SyncStatus } from "./SyncStatus.ts";
|
|
14
|
+
|
|
15
|
+
export class SyncFragmentIndex
|
|
16
|
+
implements SubAssetSync<CreateISOBMFFFileResult>
|
|
17
|
+
{
|
|
18
|
+
icon = "📋";
|
|
19
|
+
label = "fragment index";
|
|
20
|
+
syncStatus: SyncStatus = new SyncStatus(this.path);
|
|
21
|
+
fileSyncStatus: SyncStatus = new SyncStatus(
|
|
22
|
+
join(dirname(this.path), "isobmff"),
|
|
23
|
+
);
|
|
24
|
+
created: CreateISOBMFFFileResult | null = null;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
public path: string,
|
|
28
|
+
public md5: string,
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
async byteSize() {
|
|
32
|
+
return (await fs.stat(this.path)).size;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async prepare() {}
|
|
36
|
+
|
|
37
|
+
async validate() {}
|
|
38
|
+
|
|
39
|
+
async create() {
|
|
40
|
+
this.created = await createISOBMFFFile(getClient(), {
|
|
41
|
+
md5: this.md5,
|
|
42
|
+
filename: this.path.replace(/\.tracks.json$/, ""),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
isComplete() {
|
|
47
|
+
return !!this.created?.fragment_index_complete;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async upload() {
|
|
51
|
+
if (!this.created) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"Fragment index not created. Should have been prevented by .isComplete()",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
await uploadFragmentIndex(
|
|
57
|
+
getClient(),
|
|
58
|
+
this.created.id,
|
|
59
|
+
// It is unclear why we need to use Readable.from here
|
|
60
|
+
// Tests fail when using createReadStream
|
|
61
|
+
Readable.from(await fs.readFile(this.path)),
|
|
62
|
+
await this.byteSize(),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async markSynced() {
|
|
67
|
+
if (!this.created) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
"Fragment index not created. Should have been prevented by .isComplete()",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const byteSize = await this.byteSize();
|
|
73
|
+
await Promise.all([
|
|
74
|
+
this.syncStatus.markSynced({
|
|
75
|
+
version: "1",
|
|
76
|
+
complete: true,
|
|
77
|
+
id: this.created.id,
|
|
78
|
+
md5: this.md5,
|
|
79
|
+
asset_id: this.created.asset_id,
|
|
80
|
+
byte_size: byteSize,
|
|
81
|
+
}),
|
|
82
|
+
this.fileSyncStatus.markSynced({
|
|
83
|
+
version: "1",
|
|
84
|
+
complete: true,
|
|
85
|
+
id: this.created.id,
|
|
86
|
+
md5: this.md5,
|
|
87
|
+
asset_id: this.created.asset_id,
|
|
88
|
+
byte_size: byteSize,
|
|
89
|
+
}),
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { fixture, withFixtures } from "../../../test-fixtures/fixture.ts";
|
|
3
|
+
import {
|
|
4
|
+
mockCreateImageFile,
|
|
5
|
+
mockGetUploadImageFile,
|
|
6
|
+
useMSW,
|
|
7
|
+
} from "../../../test-fixtures/network.ts";
|
|
8
|
+
import { SyncImage } from "./SyncImage.ts";
|
|
9
|
+
|
|
10
|
+
describe("SyncImage", async () => {
|
|
11
|
+
const server = useMSW();
|
|
12
|
+
await withFixtures(
|
|
13
|
+
[fixture("test.png", "test.png")],
|
|
14
|
+
async ({ files: [image], cacheImage }) => {
|
|
15
|
+
test("Reads byte size", async () => {
|
|
16
|
+
const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
|
|
17
|
+
await expect(syncImage.byteSize()).resolves.toEqual(276);
|
|
18
|
+
});
|
|
19
|
+
test("prepare() probes image", async () => {
|
|
20
|
+
const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
|
|
21
|
+
await expect(syncImage.prepare()).resolves.toBeUndefined();
|
|
22
|
+
console.log(syncImage.probeResult);
|
|
23
|
+
expect(syncImage.probeResult.data.format).toMatchObject({
|
|
24
|
+
format_name: "png_pipe",
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
describe("validate()", () => {
|
|
28
|
+
test("Throws when prepare() is not called", async () => {
|
|
29
|
+
const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
|
|
30
|
+
await expect(syncImage.validate()).rejects.toThrow();
|
|
31
|
+
});
|
|
32
|
+
test.skip("throws when probe is not a supported image", async () => {});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe(".create()", () => {
|
|
36
|
+
test("Throws when prepare() is not called", async () => {
|
|
37
|
+
const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
|
|
38
|
+
await expect(syncImage.create()).rejects.toThrow();
|
|
39
|
+
});
|
|
40
|
+
test("isComplete() returns false when not created", async () => {
|
|
41
|
+
server.use(
|
|
42
|
+
mockCreateImageFile({
|
|
43
|
+
complete: false,
|
|
44
|
+
id: "123",
|
|
45
|
+
filename: "test.mp4",
|
|
46
|
+
fixture: image!,
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
|
|
50
|
+
await syncImage.prepare();
|
|
51
|
+
await syncImage.create();
|
|
52
|
+
expect(syncImage.isComplete()).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
test("isComplete() returns true when created", async () => {
|
|
55
|
+
server.use(
|
|
56
|
+
mockCreateImageFile({
|
|
57
|
+
complete: true,
|
|
58
|
+
id: "123",
|
|
59
|
+
filename: "test.mp4",
|
|
60
|
+
fixture: image!,
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
|
|
64
|
+
await syncImage.prepare();
|
|
65
|
+
await syncImage.create();
|
|
66
|
+
expect(syncImage.isComplete()).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe(".upload()", () => {
|
|
70
|
+
test("throws when not created", async () => {
|
|
71
|
+
const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
|
|
72
|
+
await syncImage.prepare();
|
|
73
|
+
// This is throwing syncronously because the error is thrown before
|
|
74
|
+
// any async operations are awaited.
|
|
75
|
+
expect(() => syncImage.upload()).toThrow();
|
|
76
|
+
});
|
|
77
|
+
test("uploads image", async () => {
|
|
78
|
+
server.use(
|
|
79
|
+
mockCreateImageFile({
|
|
80
|
+
complete: false,
|
|
81
|
+
id: "123",
|
|
82
|
+
filename: "test.mp4",
|
|
83
|
+
fixture: image!,
|
|
84
|
+
}),
|
|
85
|
+
mockGetUploadImageFile({
|
|
86
|
+
id: "123",
|
|
87
|
+
fixture: image!,
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
|
|
91
|
+
await syncImage.prepare();
|
|
92
|
+
await syncImage.create();
|
|
93
|
+
await expect(syncImage.upload()).resolves.toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe(".markSynced()", () => {
|
|
97
|
+
test("throws when not created", async () => {
|
|
98
|
+
const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
|
|
99
|
+
await expect(syncImage.markSynced()).rejects.toThrow();
|
|
100
|
+
});
|
|
101
|
+
test("marks synced", async () => {
|
|
102
|
+
server.use(
|
|
103
|
+
mockCreateImageFile({
|
|
104
|
+
complete: true,
|
|
105
|
+
id: "123",
|
|
106
|
+
fixture: image!,
|
|
107
|
+
}),
|
|
108
|
+
mockGetUploadImageFile({
|
|
109
|
+
id: "123",
|
|
110
|
+
fixture: image!,
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
const syncImage = new SyncImage(await cacheImage(image!), image!.md5);
|
|
114
|
+
await syncImage.prepare();
|
|
115
|
+
await syncImage.create();
|
|
116
|
+
await syncImage.upload();
|
|
117
|
+
await syncImage.markSynced();
|
|
118
|
+
await expect(syncImage.syncStatus.isSynced()).resolves.toBe(true);
|
|
119
|
+
await expect(syncImage.syncStatus.readInfo()).resolves.toEqual({
|
|
120
|
+
version: "1",
|
|
121
|
+
complete: true,
|
|
122
|
+
id: "123",
|
|
123
|
+
md5: image!.md5,
|
|
124
|
+
asset_id: `${image!.md5}:test.png`,
|
|
125
|
+
byte_size: 276,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path, { basename } from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type CreateImageFileResult,
|
|
7
|
+
createImageFile,
|
|
8
|
+
uploadImageFile,
|
|
9
|
+
} from "@editframe/api";
|
|
10
|
+
|
|
11
|
+
import { Probe } from "@editframe/assets";
|
|
12
|
+
|
|
13
|
+
import { getClient } from "../../utils/index.ts";
|
|
14
|
+
import type { SubAssetSync } from "./SubAssetSync.ts";
|
|
15
|
+
import { SyncStatus } from "./SyncStatus.ts";
|
|
16
|
+
|
|
17
|
+
export class SyncImage implements SubAssetSync<CreateImageFileResult> {
|
|
18
|
+
icon = "🖼️";
|
|
19
|
+
label = "image";
|
|
20
|
+
syncStatus: SyncStatus = new SyncStatus(this.path);
|
|
21
|
+
created: CreateImageFileResult | null = null;
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
public path: string,
|
|
25
|
+
public md5: string,
|
|
26
|
+
) {}
|
|
27
|
+
|
|
28
|
+
private _probeResult: Probe | null = null;
|
|
29
|
+
|
|
30
|
+
async prepare() {
|
|
31
|
+
this._probeResult = await Probe.probePath(this.path);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get probeResult() {
|
|
35
|
+
if (!this._probeResult) {
|
|
36
|
+
throw new Error("Probe result not found. Call prepare() first.");
|
|
37
|
+
}
|
|
38
|
+
return this._probeResult;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get extension() {
|
|
42
|
+
return path.extname(this.path).slice(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async byteSize() {
|
|
46
|
+
return (await fs.stat(this.path)).size;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async validate() {
|
|
50
|
+
const [videoProbe] = this.probeResult.videoStreams;
|
|
51
|
+
if (!videoProbe) {
|
|
52
|
+
throw new Error(`No media info found in image: ${this.path}`);
|
|
53
|
+
}
|
|
54
|
+
const ext = this.extension;
|
|
55
|
+
if (!(ext === "jpg" || ext === "jpeg" || ext === "png" || ext === "webp")) {
|
|
56
|
+
throw new Error(`Invalid image format: ${this.path}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async create() {
|
|
60
|
+
const byteSize = (await fs.stat(this.path)).size;
|
|
61
|
+
const [videoProbe] = this.probeResult.videoStreams;
|
|
62
|
+
if (!videoProbe) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"No video stream found in image. Should have been prevented by .validate()",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
this.created = await createImageFile(getClient(), {
|
|
68
|
+
md5: this.md5,
|
|
69
|
+
filename: basename(this.path),
|
|
70
|
+
width: videoProbe.width,
|
|
71
|
+
height: videoProbe.height,
|
|
72
|
+
mime_type: `image/${this.extension}` as
|
|
73
|
+
| "image/jpeg"
|
|
74
|
+
| "image/png"
|
|
75
|
+
| "image/jpg"
|
|
76
|
+
| "image/webp",
|
|
77
|
+
byte_size: byteSize,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
isComplete() {
|
|
81
|
+
return !!this.created?.complete;
|
|
82
|
+
}
|
|
83
|
+
upload() {
|
|
84
|
+
if (!this.created) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
"Image not created. Should have been prevented by .isComplete()",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return uploadImageFile(
|
|
90
|
+
getClient(),
|
|
91
|
+
this.created.id,
|
|
92
|
+
createReadStream(this.path),
|
|
93
|
+
Number.parseInt(this.probeResult.format.size || "0"),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
async markSynced() {
|
|
97
|
+
if (!this.created) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
"Image not created. Should have been prevented by .isComplete()",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const byteSize = await this.byteSize();
|
|
103
|
+
return this.syncStatus.markSynced({
|
|
104
|
+
version: "1",
|
|
105
|
+
complete: true,
|
|
106
|
+
id: this.created.id,
|
|
107
|
+
md5: this.md5,
|
|
108
|
+
asset_id: this.created.asset_id,
|
|
109
|
+
byte_size: byteSize,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
const SYNC_VERSION = "1";
|
|
6
|
+
|
|
7
|
+
const SyncStatusSchema = z.object({
|
|
8
|
+
version: z.string(),
|
|
9
|
+
complete: z.boolean(),
|
|
10
|
+
id: z.string(),
|
|
11
|
+
md5: z.string(),
|
|
12
|
+
asset_id: z.string(),
|
|
13
|
+
byte_size: z.number(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export interface SyncStatusInfo extends z.infer<typeof SyncStatusSchema> {}
|
|
17
|
+
|
|
18
|
+
export class SyncStatus {
|
|
19
|
+
constructor(private basePath: string) {}
|
|
20
|
+
|
|
21
|
+
infoPath = `${this.basePath}.info`;
|
|
22
|
+
|
|
23
|
+
async isSynced() {
|
|
24
|
+
const syncInfo = await this.readInfo();
|
|
25
|
+
if (!syncInfo) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return syncInfo.version === SYNC_VERSION && syncInfo.complete;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async readInfo() {
|
|
32
|
+
try {
|
|
33
|
+
const info = await fs.readFile(this.infoPath, "utf-8");
|
|
34
|
+
return SyncStatusSchema.parse(JSON.parse(info));
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (
|
|
37
|
+
error instanceof Error &&
|
|
38
|
+
"code" in error &&
|
|
39
|
+
error.code === "ENOENT"
|
|
40
|
+
) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async markSynced(info: SyncStatusInfo) {
|
|
48
|
+
process.stderr.write(`✏️ Marking asset as synced: ${this.basePath}\n`);
|
|
49
|
+
await fs.writeFile(this.infoPath, JSON.stringify(info, null, 2), "utf-8");
|
|
50
|
+
}
|
|
51
|
+
}
|