@sourcepress/media 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test.log +16 -0
- package/dist/__tests__/git-storage.test.d.ts +2 -0
- package/dist/__tests__/git-storage.test.d.ts.map +1 -0
- package/dist/__tests__/git-storage.test.js +137 -0
- package/dist/__tests__/git-storage.test.js.map +1 -0
- package/dist/__tests__/registry.test.d.ts +2 -0
- package/dist/__tests__/registry.test.d.ts.map +1 -0
- package/dist/__tests__/registry.test.js +90 -0
- package/dist/__tests__/registry.test.js.map +1 -0
- package/dist/__tests__/validation.test.d.ts +2 -0
- package/dist/__tests__/validation.test.d.ts.map +1 -0
- package/dist/__tests__/validation.test.js +49 -0
- package/dist/__tests__/validation.test.js.map +1 -0
- package/dist/git-storage.d.ts +20 -0
- package/dist/git-storage.d.ts.map +1 -0
- package/dist/git-storage.js +87 -0
- package/dist/git-storage.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/interface.d.ts +34 -0
- package/dist/interface.d.ts.map +1 -0
- package/dist/interface.js +2 -0
- package/dist/interface.js.map +1 -0
- package/dist/registry.d.ts +42 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +88 -0
- package/dist/registry.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +12 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +51 -0
- package/dist/validation.js.map +1 -0
- package/package.json +26 -0
- package/src/__tests__/git-storage.test.ts +160 -0
- package/src/__tests__/registry.test.ts +116 -0
- package/src/__tests__/validation.test.ts +58 -0
- package/src/git-storage.ts +109 -0
- package/src/index.ts +4 -0
- package/src/interface.ts +42 -0
- package/src/registry.ts +105 -0
- package/src/types.ts +1 -0
- package/src/validation.ts +70 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
> @sourcepress/media@0.1.0 test /Users/fabianvontiedemann/Developer/org/sourcepress/packages/media
|
|
3
|
+
> vitest run
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
RUN v3.2.4 /Users/fabianvontiedemann/Developer/org/sourcepress/packages/media
|
|
7
|
+
|
|
8
|
+
✓ src/__tests__/registry.test.ts (7 tests) 4ms
|
|
9
|
+
✓ src/__tests__/git-storage.test.ts (6 tests) 4ms
|
|
10
|
+
✓ src/__tests__/validation.test.ts (10 tests) 2ms
|
|
11
|
+
|
|
12
|
+
Test Files 3 passed (3)
|
|
13
|
+
Tests 23 passed (23)
|
|
14
|
+
Start at 00:37:58
|
|
15
|
+
Duration 1.03s (transform 77ms, setup 0ms, collect 146ms, tests 10ms, environment 0ms, prepare 503ms)
|
|
16
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-storage.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/git-storage.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { GitMediaStorage } from "../git-storage.js";
|
|
3
|
+
// Mock crypto.subtle for hash computation
|
|
4
|
+
vi.stubGlobal("crypto", {
|
|
5
|
+
subtle: {
|
|
6
|
+
digest: vi.fn().mockResolvedValue(new Uint8Array(32).buffer),
|
|
7
|
+
},
|
|
8
|
+
});
|
|
9
|
+
function createMockGitHub() {
|
|
10
|
+
return {
|
|
11
|
+
getFile: vi.fn().mockResolvedValue(null),
|
|
12
|
+
createOrUpdateFile: vi.fn().mockResolvedValue({ sha: "file-sha", commit_sha: "commit-sha" }),
|
|
13
|
+
owner: "test",
|
|
14
|
+
repo: "test",
|
|
15
|
+
branch: "main",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const defaultConfig = {
|
|
19
|
+
storage: "git",
|
|
20
|
+
path: "assets",
|
|
21
|
+
registry: "media.json",
|
|
22
|
+
};
|
|
23
|
+
describe("GitMediaStorage", () => {
|
|
24
|
+
it("uploads a file and registers it", async () => {
|
|
25
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock GitHubClient
|
|
26
|
+
const github = createMockGitHub();
|
|
27
|
+
const storage = new GitMediaStorage(github, defaultConfig);
|
|
28
|
+
const result = await storage.upload({
|
|
29
|
+
file: Buffer.from("fake-image-data"),
|
|
30
|
+
path: "hero.webp",
|
|
31
|
+
content_type: "image/webp",
|
|
32
|
+
source: "uploaded",
|
|
33
|
+
uploaded_by: "test-user",
|
|
34
|
+
alt: "Hero image",
|
|
35
|
+
});
|
|
36
|
+
expect(result.path).toBe("assets/hero.webp");
|
|
37
|
+
expect(result.content_type).toBe("image/webp");
|
|
38
|
+
expect(result.source).toBe("uploaded");
|
|
39
|
+
expect(result.uploaded_by).toBe("test-user");
|
|
40
|
+
expect(result.alt).toBe("Hero image");
|
|
41
|
+
// Should have uploaded the file
|
|
42
|
+
expect(github.createOrUpdateFile).toHaveBeenCalledTimes(2); // file + registry
|
|
43
|
+
});
|
|
44
|
+
it("rejects disallowed content type", async () => {
|
|
45
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock GitHubClient
|
|
46
|
+
const github = createMockGitHub();
|
|
47
|
+
const storage = new GitMediaStorage(github, defaultConfig);
|
|
48
|
+
await expect(storage.upload({
|
|
49
|
+
file: Buffer.from("data"),
|
|
50
|
+
path: "malware.exe",
|
|
51
|
+
content_type: "application/x-executable",
|
|
52
|
+
source: "uploaded",
|
|
53
|
+
uploaded_by: "test-user",
|
|
54
|
+
})).rejects.toThrow("not allowed");
|
|
55
|
+
});
|
|
56
|
+
it("rejects file exceeding max size", async () => {
|
|
57
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock GitHubClient
|
|
58
|
+
const github = createMockGitHub();
|
|
59
|
+
const storage = new GitMediaStorage(github, {
|
|
60
|
+
...defaultConfig,
|
|
61
|
+
maxSizeMb: 1,
|
|
62
|
+
});
|
|
63
|
+
const largeBuffer = Buffer.alloc(2 * 1024 * 1024); // 2MB
|
|
64
|
+
await expect(storage.upload({
|
|
65
|
+
file: largeBuffer,
|
|
66
|
+
path: "large.png",
|
|
67
|
+
content_type: "image/png",
|
|
68
|
+
source: "uploaded",
|
|
69
|
+
uploaded_by: "test-user",
|
|
70
|
+
})).rejects.toThrow("exceeds maximum");
|
|
71
|
+
});
|
|
72
|
+
it("sanitizes upload paths", async () => {
|
|
73
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock GitHubClient
|
|
74
|
+
const github = createMockGitHub();
|
|
75
|
+
const storage = new GitMediaStorage(github, defaultConfig);
|
|
76
|
+
const result = await storage.upload({
|
|
77
|
+
file: Buffer.from("data"),
|
|
78
|
+
path: "../../../HERO.WebP",
|
|
79
|
+
content_type: "image/webp",
|
|
80
|
+
source: "uploaded",
|
|
81
|
+
uploaded_by: "test-user",
|
|
82
|
+
});
|
|
83
|
+
expect(result.path).toBe("assets/hero.webp");
|
|
84
|
+
});
|
|
85
|
+
it("lists media entries", async () => {
|
|
86
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock GitHubClient
|
|
87
|
+
const github = createMockGitHub();
|
|
88
|
+
// Pre-populate registry via getFile mock
|
|
89
|
+
const registry = {
|
|
90
|
+
"assets/a.webp": {
|
|
91
|
+
path: "assets/a.webp",
|
|
92
|
+
content_type: "image/webp",
|
|
93
|
+
size_bytes: 1000,
|
|
94
|
+
hash: "aaa",
|
|
95
|
+
source: "uploaded",
|
|
96
|
+
uploaded_at: "2026-04-04",
|
|
97
|
+
uploaded_by: "test",
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
github.getFile.mockResolvedValueOnce({
|
|
101
|
+
path: "media.json",
|
|
102
|
+
sha: "reg-sha",
|
|
103
|
+
content: JSON.stringify(registry),
|
|
104
|
+
encoding: "utf-8",
|
|
105
|
+
});
|
|
106
|
+
const storage = new GitMediaStorage(github, defaultConfig);
|
|
107
|
+
const list = await storage.list();
|
|
108
|
+
expect(list).toHaveLength(1);
|
|
109
|
+
expect(list[0].path).toBe("assets/a.webp");
|
|
110
|
+
});
|
|
111
|
+
it("updates metadata for existing file", async () => {
|
|
112
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock GitHubClient
|
|
113
|
+
const github = createMockGitHub();
|
|
114
|
+
const existingRef = {
|
|
115
|
+
path: "assets/hero.webp",
|
|
116
|
+
content_type: "image/webp",
|
|
117
|
+
size_bytes: 5000,
|
|
118
|
+
hash: "abc",
|
|
119
|
+
source: "uploaded",
|
|
120
|
+
uploaded_at: "2026-04-04",
|
|
121
|
+
uploaded_by: "test",
|
|
122
|
+
};
|
|
123
|
+
github.getFile.mockResolvedValueOnce({
|
|
124
|
+
path: "media.json",
|
|
125
|
+
sha: "reg-sha",
|
|
126
|
+
content: JSON.stringify({ "assets/hero.webp": existingRef }),
|
|
127
|
+
encoding: "utf-8",
|
|
128
|
+
});
|
|
129
|
+
const storage = new GitMediaStorage(github, defaultConfig);
|
|
130
|
+
const updated = await storage.updateMeta("assets/hero.webp", {
|
|
131
|
+
alt: "Updated alt text",
|
|
132
|
+
});
|
|
133
|
+
expect(updated.alt).toBe("Updated alt text");
|
|
134
|
+
expect(updated.path).toBe("assets/hero.webp");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
//# sourceMappingURL=git-storage.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-storage.test.js","sourceRoot":"","sources":["../../src/__tests__/git-storage.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/registry.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { MediaRegistryManager } from "../registry.js";
|
|
3
|
+
// Mock GitHubClient
|
|
4
|
+
function createMockGitHub(existingRegistry) {
|
|
5
|
+
const content = existingRegistry ? JSON.stringify(existingRegistry) : null;
|
|
6
|
+
return {
|
|
7
|
+
getFile: vi
|
|
8
|
+
.fn()
|
|
9
|
+
.mockResolvedValue(content ? { path: "media.json", sha: "abc123", content, encoding: "utf-8" } : null),
|
|
10
|
+
createOrUpdateFile: vi.fn().mockResolvedValue({ sha: "new-sha", commit_sha: "commit-sha" }),
|
|
11
|
+
owner: "test",
|
|
12
|
+
repo: "test",
|
|
13
|
+
branch: "main",
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const sampleRef = {
|
|
17
|
+
path: "assets/hero.webp",
|
|
18
|
+
content_type: "image/webp",
|
|
19
|
+
size_bytes: 50000,
|
|
20
|
+
hash: "abc123def456",
|
|
21
|
+
source: "uploaded",
|
|
22
|
+
uploaded_at: "2026-04-04T10:00:00Z",
|
|
23
|
+
uploaded_by: "test-user",
|
|
24
|
+
};
|
|
25
|
+
describe("MediaRegistryManager", () => {
|
|
26
|
+
it("loads empty registry when file does not exist", async () => {
|
|
27
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock GitHubClient
|
|
28
|
+
const github = createMockGitHub();
|
|
29
|
+
const manager = new MediaRegistryManager(github, "media.json");
|
|
30
|
+
const registry = await manager.load();
|
|
31
|
+
expect(registry).toEqual({});
|
|
32
|
+
});
|
|
33
|
+
it("loads existing registry from Git", async () => {
|
|
34
|
+
const existing = { "assets/hero.webp": sampleRef };
|
|
35
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock GitHubClient
|
|
36
|
+
const github = createMockGitHub(existing);
|
|
37
|
+
const manager = new MediaRegistryManager(github, "media.json");
|
|
38
|
+
const registry = await manager.load();
|
|
39
|
+
expect(registry["assets/hero.webp"]).toEqual(sampleRef);
|
|
40
|
+
});
|
|
41
|
+
it("sets and retrieves an entry", async () => {
|
|
42
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock GitHubClient
|
|
43
|
+
const github = createMockGitHub();
|
|
44
|
+
const manager = new MediaRegistryManager(github, "media.json");
|
|
45
|
+
await manager.set("assets/new.png", sampleRef);
|
|
46
|
+
const result = await manager.get("assets/new.png");
|
|
47
|
+
expect(result).toEqual(sampleRef);
|
|
48
|
+
});
|
|
49
|
+
it("removes an entry", async () => {
|
|
50
|
+
const existing = { "assets/hero.webp": sampleRef };
|
|
51
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock GitHubClient
|
|
52
|
+
const github = createMockGitHub(existing);
|
|
53
|
+
const manager = new MediaRegistryManager(github, "media.json");
|
|
54
|
+
await manager.remove("assets/hero.webp");
|
|
55
|
+
const result = await manager.get("assets/hero.webp");
|
|
56
|
+
expect(result).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
it("lists entries with prefix filter", async () => {
|
|
59
|
+
const existing = {
|
|
60
|
+
"assets/cases/hero.webp": { ...sampleRef, path: "assets/cases/hero.webp" },
|
|
61
|
+
"assets/team/anna.jpg": { ...sampleRef, path: "assets/team/anna.jpg" },
|
|
62
|
+
"assets/cases/detail.png": { ...sampleRef, path: "assets/cases/detail.png" },
|
|
63
|
+
};
|
|
64
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock GitHubClient
|
|
65
|
+
const github = createMockGitHub(existing);
|
|
66
|
+
const manager = new MediaRegistryManager(github, "media.json");
|
|
67
|
+
const casesOnly = await manager.list("assets/cases/");
|
|
68
|
+
expect(casesOnly).toHaveLength(2);
|
|
69
|
+
});
|
|
70
|
+
it("saves registry to Git", async () => {
|
|
71
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock GitHubClient
|
|
72
|
+
const github = createMockGitHub();
|
|
73
|
+
const manager = new MediaRegistryManager(github, "media.json");
|
|
74
|
+
await manager.set("assets/hero.webp", sampleRef);
|
|
75
|
+
await manager.save("media: add hero image");
|
|
76
|
+
expect(github.createOrUpdateFile).toHaveBeenCalledWith("media.json", expect.stringContaining("assets/hero.webp"), "media: add hero image", undefined, undefined);
|
|
77
|
+
});
|
|
78
|
+
it("invalidates cache", async () => {
|
|
79
|
+
const existing = { "assets/hero.webp": sampleRef };
|
|
80
|
+
// biome-ignore lint/suspicious/noExplicitAny: mock GitHubClient
|
|
81
|
+
const github = createMockGitHub(existing);
|
|
82
|
+
const manager = new MediaRegistryManager(github, "media.json");
|
|
83
|
+
await manager.load();
|
|
84
|
+
manager.invalidate();
|
|
85
|
+
// Should call getFile again after invalidation
|
|
86
|
+
await manager.load();
|
|
87
|
+
expect(github.getFile).toHaveBeenCalledTimes(2);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
//# sourceMappingURL=registry.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.test.js","sourceRoot":"","sources":["../../src/__tests__/registry.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/validation.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { sanitizePath, validateUpload } from "../validation.js";
|
|
3
|
+
describe("validateUpload", () => {
|
|
4
|
+
it("accepts valid image upload", () => {
|
|
5
|
+
const result = validateUpload("image/webp", 500_000);
|
|
6
|
+
expect(result.valid).toBe(true);
|
|
7
|
+
expect(result.error).toBeUndefined();
|
|
8
|
+
});
|
|
9
|
+
it("rejects disallowed content type", () => {
|
|
10
|
+
const result = validateUpload("application/zip", 1000);
|
|
11
|
+
expect(result.valid).toBe(false);
|
|
12
|
+
expect(result.error).toContain("not allowed");
|
|
13
|
+
});
|
|
14
|
+
it("rejects file exceeding max size", () => {
|
|
15
|
+
const result = validateUpload("image/png", 20 * 1024 * 1024);
|
|
16
|
+
expect(result.valid).toBe(false);
|
|
17
|
+
expect(result.error).toContain("exceeds maximum");
|
|
18
|
+
});
|
|
19
|
+
it("uses custom allowed types from config", () => {
|
|
20
|
+
const result = validateUpload("application/zip", 1000, {
|
|
21
|
+
allowedTypes: ["application/zip"],
|
|
22
|
+
});
|
|
23
|
+
expect(result.valid).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
it("uses custom max size from config", () => {
|
|
26
|
+
const result = validateUpload("image/png", 50 * 1024 * 1024, {
|
|
27
|
+
maxSizeMb: 100,
|
|
28
|
+
});
|
|
29
|
+
expect(result.valid).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe("sanitizePath", () => {
|
|
33
|
+
it("normalizes backslashes to forward slashes", () => {
|
|
34
|
+
expect(sanitizePath("assets\\images\\hero.png")).toBe("assets/images/hero.png");
|
|
35
|
+
});
|
|
36
|
+
it("removes directory traversal attempts", () => {
|
|
37
|
+
expect(sanitizePath("../../../etc/passwd")).toBe("etc/passwd");
|
|
38
|
+
});
|
|
39
|
+
it("collapses multiple slashes", () => {
|
|
40
|
+
expect(sanitizePath("assets///images//hero.png")).toBe("assets/images/hero.png");
|
|
41
|
+
});
|
|
42
|
+
it("lowercases the path", () => {
|
|
43
|
+
expect(sanitizePath("Assets/HERO.PNG")).toBe("assets/hero.png");
|
|
44
|
+
});
|
|
45
|
+
it("removes leading slash", () => {
|
|
46
|
+
expect(sanitizePath("/assets/hero.png")).toBe("assets/hero.png");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
//# sourceMappingURL=validation.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.test.js","sourceRoot":"","sources":["../../src/__tests__/validation.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MediaConfig, MediaRef, MediaUploadInput } from "@sourcepress/core";
|
|
2
|
+
import type { GitHubClient } from "@sourcepress/github";
|
|
3
|
+
import type { MediaStorage } from "./interface.js";
|
|
4
|
+
/**
|
|
5
|
+
* GitMediaStorage stores media files directly in the Git repository.
|
|
6
|
+
* Best for small sites with < 50 images.
|
|
7
|
+
*/
|
|
8
|
+
export declare class GitMediaStorage implements MediaStorage {
|
|
9
|
+
private github;
|
|
10
|
+
private registry;
|
|
11
|
+
private config;
|
|
12
|
+
constructor(github: GitHubClient, config: MediaConfig);
|
|
13
|
+
upload(input: MediaUploadInput): Promise<MediaRef>;
|
|
14
|
+
get(path: string): Promise<Buffer | null>;
|
|
15
|
+
delete(path: string): Promise<void>;
|
|
16
|
+
list(prefix?: string): Promise<MediaRef[]>;
|
|
17
|
+
getMeta(path: string): Promise<MediaRef | null>;
|
|
18
|
+
updateMeta(path: string, updates: Partial<Pick<MediaRef, "alt" | "source" | "generated_by" | "prompt">>): Promise<MediaRef>;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=git-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-storage.d.ts","sourceRoot":"","sources":["../src/git-storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAanD;;;GAGG;AACH,qBAAa,eAAgB,YAAW,YAAY;IACnD,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,MAAM,CAAc;gBAEhB,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW;IAM/C,MAAM,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAuClD,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAMzC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUnC,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAI1C,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAI/C,UAAU,CACf,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,cAAc,GAAG,QAAQ,CAAC,CAAC,GAC5E,OAAO,CAAC,QAAQ,CAAC;CAYpB"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { MediaRegistryManager } from "./registry.js";
|
|
2
|
+
import { sanitizePath, validateUpload } from "./validation.js";
|
|
3
|
+
/**
|
|
4
|
+
* Compute a simple hash from a Buffer using Web Crypto API.
|
|
5
|
+
*/
|
|
6
|
+
async function computeHash(data) {
|
|
7
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
8
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
9
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* GitMediaStorage stores media files directly in the Git repository.
|
|
13
|
+
* Best for small sites with < 50 images.
|
|
14
|
+
*/
|
|
15
|
+
export class GitMediaStorage {
|
|
16
|
+
github;
|
|
17
|
+
registry;
|
|
18
|
+
config;
|
|
19
|
+
constructor(github, config) {
|
|
20
|
+
this.github = github;
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.registry = new MediaRegistryManager(github, config.registry);
|
|
23
|
+
}
|
|
24
|
+
async upload(input) {
|
|
25
|
+
const sanitized = sanitizePath(input.path);
|
|
26
|
+
const fullPath = `${this.config.path}/${sanitized}`;
|
|
27
|
+
// Validate
|
|
28
|
+
const validation = validateUpload(input.content_type, input.file.length, this.config);
|
|
29
|
+
if (!validation.valid) {
|
|
30
|
+
throw new Error(validation.error);
|
|
31
|
+
}
|
|
32
|
+
// Compute hash
|
|
33
|
+
const hash = await computeHash(input.file);
|
|
34
|
+
// Upload file to Git — pass as latin1 string so createOrUpdateFile's
|
|
35
|
+
// internal btoa() produces valid base64 (no double-encoding).
|
|
36
|
+
const content = input.file.toString("latin1");
|
|
37
|
+
await this.github.createOrUpdateFile(fullPath, content, `media: upload ${sanitized}`);
|
|
38
|
+
// Create MediaRef
|
|
39
|
+
const ref = {
|
|
40
|
+
path: fullPath,
|
|
41
|
+
content_type: input.content_type,
|
|
42
|
+
size_bytes: input.file.length,
|
|
43
|
+
hash,
|
|
44
|
+
alt: input.alt,
|
|
45
|
+
source: input.source,
|
|
46
|
+
generated_by: input.generated_by,
|
|
47
|
+
prompt: input.prompt,
|
|
48
|
+
uploaded_at: new Date().toISOString(),
|
|
49
|
+
uploaded_by: input.uploaded_by,
|
|
50
|
+
};
|
|
51
|
+
// Register in media.json
|
|
52
|
+
await this.registry.set(fullPath, ref);
|
|
53
|
+
await this.registry.save(`media: register ${sanitized}`);
|
|
54
|
+
return ref;
|
|
55
|
+
}
|
|
56
|
+
async get(path) {
|
|
57
|
+
const file = await this.github.getFile(path);
|
|
58
|
+
if (!file)
|
|
59
|
+
return null;
|
|
60
|
+
return Buffer.from(file.content, "base64");
|
|
61
|
+
}
|
|
62
|
+
async delete(path) {
|
|
63
|
+
// Remove from registry
|
|
64
|
+
await this.registry.remove(path);
|
|
65
|
+
await this.registry.save(`media: delete ${path}`);
|
|
66
|
+
// Note: Git doesn't truly "delete" — we remove from registry.
|
|
67
|
+
// The file remains in Git history. For actual file deletion,
|
|
68
|
+
// a future version could use the GitHub Contents API DELETE.
|
|
69
|
+
}
|
|
70
|
+
async list(prefix) {
|
|
71
|
+
return this.registry.list(prefix);
|
|
72
|
+
}
|
|
73
|
+
async getMeta(path) {
|
|
74
|
+
return this.registry.get(path);
|
|
75
|
+
}
|
|
76
|
+
async updateMeta(path, updates) {
|
|
77
|
+
const existing = await this.registry.get(path);
|
|
78
|
+
if (!existing) {
|
|
79
|
+
throw new Error(`Media not found: ${path}`);
|
|
80
|
+
}
|
|
81
|
+
const updated = { ...existing, ...updates };
|
|
82
|
+
await this.registry.set(path, updated);
|
|
83
|
+
await this.registry.save(`media: update metadata for ${path}`);
|
|
84
|
+
return updated;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=git-storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-storage.js","sourceRoot":"","sources":["../src/git-storage.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { MediaRef, MediaUploadInput } from "@sourcepress/core";
|
|
2
|
+
/**
|
|
3
|
+
* Pluggable media storage interface.
|
|
4
|
+
* Implementations: GitMediaStorage (default), R2MediaStorage, S3MediaStorage (future).
|
|
5
|
+
*/
|
|
6
|
+
export interface MediaStorage {
|
|
7
|
+
/**
|
|
8
|
+
* Upload a file and register it in the media registry.
|
|
9
|
+
* Returns the MediaRef with hash and metadata.
|
|
10
|
+
*/
|
|
11
|
+
upload(input: MediaUploadInput): Promise<MediaRef>;
|
|
12
|
+
/**
|
|
13
|
+
* Get file contents by path.
|
|
14
|
+
* Returns null if the file does not exist.
|
|
15
|
+
*/
|
|
16
|
+
get(path: string): Promise<Buffer | null>;
|
|
17
|
+
/**
|
|
18
|
+
* Delete a file and remove it from the registry.
|
|
19
|
+
*/
|
|
20
|
+
delete(path: string): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* List all media entries, optionally filtered by path prefix.
|
|
23
|
+
*/
|
|
24
|
+
list(prefix?: string): Promise<MediaRef[]>;
|
|
25
|
+
/**
|
|
26
|
+
* Get metadata for a single file from the registry.
|
|
27
|
+
*/
|
|
28
|
+
getMeta(path: string): Promise<MediaRef | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Update metadata for an existing file (e.g., alt-text).
|
|
31
|
+
*/
|
|
32
|
+
updateMeta(path: string, updates: Partial<Pick<MediaRef, "alt" | "source" | "generated_by" | "prompt">>): Promise<MediaRef>;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=interface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../src/interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAEpE;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC5B;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEnD;;;OAGG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAE1C;;OAEG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpC;;OAEG;IACH,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAE3C;;OAEG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IAEhD;;OAEG;IACH,UAAU,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,cAAc,GAAG,QAAQ,CAAC,CAAC,GAC5E,OAAO,CAAC,QAAQ,CAAC,CAAC;CACrB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interface.js","sourceRoot":"","sources":["../src/interface.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { MediaRef, MediaRegistry } from "@sourcepress/core";
|
|
2
|
+
import type { GitHubClient } from "@sourcepress/github";
|
|
3
|
+
/**
|
|
4
|
+
* MediaRegistryManager reads/writes the media.json file in Git.
|
|
5
|
+
* This is the source of truth for all media metadata.
|
|
6
|
+
*/
|
|
7
|
+
export declare class MediaRegistryManager {
|
|
8
|
+
private github;
|
|
9
|
+
private registryPath;
|
|
10
|
+
private cache;
|
|
11
|
+
private registrySha;
|
|
12
|
+
constructor(github: GitHubClient, registryPath: string);
|
|
13
|
+
/**
|
|
14
|
+
* Load the full registry from Git.
|
|
15
|
+
*/
|
|
16
|
+
load(): Promise<MediaRegistry>;
|
|
17
|
+
/**
|
|
18
|
+
* Save the registry back to Git.
|
|
19
|
+
*/
|
|
20
|
+
save(message: string): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Add or update an entry in the registry.
|
|
23
|
+
*/
|
|
24
|
+
set(path: string, ref: MediaRef): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Get a single entry.
|
|
27
|
+
*/
|
|
28
|
+
get(path: string): Promise<MediaRef | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Remove an entry from the registry.
|
|
31
|
+
*/
|
|
32
|
+
remove(path: string): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* List entries, optionally filtered by prefix.
|
|
35
|
+
*/
|
|
36
|
+
list(prefix?: string): Promise<MediaRef[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Invalidate the in-memory cache.
|
|
39
|
+
*/
|
|
40
|
+
invalidate(): void;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD;;;GAGG;AACH,qBAAa,oBAAoB;IAChC,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,KAAK,CAA8B;IAC3C,OAAO,CAAC,WAAW,CAAuB;gBAE9B,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM;IAKtD;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,aAAa,CAAC;IAoBpC;;OAEG;IACG,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY1C;;OAEG;IACG,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAMrD;;OAEG;IACG,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAKjD;;OAEG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOzC;;OAEG;IACG,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAWhD;;OAEG;IACH,UAAU,IAAI,IAAI;CAIlB"}
|