@jant/core 0.3.8 → 0.3.9
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/app.js +7 -4
- package/dist/db/schema.js +2 -1
- package/dist/lib/image.js +39 -15
- package/dist/lib/media-helpers.js +14 -8
- package/dist/lib/storage.js +164 -0
- package/dist/routes/api/posts.js +12 -7
- package/dist/routes/api/timeline.js +3 -2
- package/dist/routes/api/upload.js +27 -20
- package/dist/routes/dash/media.js +24 -14
- package/dist/routes/dash/posts.js +4 -1
- package/dist/routes/feed/rss.js +3 -2
- package/dist/routes/pages/home.js +3 -2
- package/dist/routes/pages/post.js +9 -5
- package/dist/services/media.js +7 -5
- package/dist/theme/components/PostForm.js +4 -3
- package/dist/types.js +32 -0
- package/package.json +2 -1
- package/src/__tests__/helpers/app.ts +1 -0
- package/src/__tests__/helpers/db.ts +10 -0
- package/src/app.tsx +8 -7
- package/src/db/migrations/0004_add_storage_provider.sql +3 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +2 -1
- package/src/i18n/locales/en.po +67 -67
- package/src/i18n/locales/zh-Hans.po +67 -67
- package/src/i18n/locales/zh-Hant.po +67 -67
- package/src/lib/__tests__/image.test.ts +96 -0
- package/src/lib/__tests__/storage.test.ts +162 -0
- package/src/lib/image.ts +46 -16
- package/src/lib/media-helpers.ts +29 -18
- package/src/lib/storage.ts +236 -0
- package/src/routes/api/__tests__/posts.test.ts +8 -8
- package/src/routes/api/posts.ts +20 -6
- package/src/routes/api/timeline.tsx +8 -1
- package/src/routes/api/upload.ts +44 -21
- package/src/routes/dash/media.tsx +40 -8
- package/src/routes/dash/posts.tsx +5 -0
- package/src/routes/feed/rss.ts +3 -2
- package/src/routes/pages/home.tsx +8 -1
- package/src/routes/pages/post.tsx +29 -17
- package/src/services/__tests__/media.test.ts +44 -26
- package/src/services/media.ts +10 -7
- package/src/theme/components/PostForm.tsx +13 -2
- package/src/types.ts +41 -1
package/src/routes/api/posts.ts
CHANGED
|
@@ -12,7 +12,11 @@ import {
|
|
|
12
12
|
validateMediaForPostType,
|
|
13
13
|
} from "../../lib/schemas.js";
|
|
14
14
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
getMediaUrl,
|
|
17
|
+
getImageUrl,
|
|
18
|
+
getPublicUrlForProvider,
|
|
19
|
+
} from "../../lib/image.js";
|
|
16
20
|
|
|
17
21
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
18
22
|
|
|
@@ -25,8 +29,14 @@ function toMediaAttachment(
|
|
|
25
29
|
m: Media,
|
|
26
30
|
r2PublicUrl?: string,
|
|
27
31
|
imageTransformUrl?: string,
|
|
32
|
+
s3PublicUrl?: string,
|
|
28
33
|
) {
|
|
29
|
-
const
|
|
34
|
+
const publicUrl = getPublicUrlForProvider(
|
|
35
|
+
m.provider,
|
|
36
|
+
r2PublicUrl,
|
|
37
|
+
s3PublicUrl,
|
|
38
|
+
);
|
|
39
|
+
const url = getMediaUrl(m.id, m.storageKey, publicUrl);
|
|
30
40
|
const previewUrl = getImageUrl(url, imageTransformUrl, {
|
|
31
41
|
width: 400,
|
|
32
42
|
quality: 80,
|
|
@@ -66,13 +76,14 @@ postsApiRoutes.get("/", async (c) => {
|
|
|
66
76
|
const mediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
67
77
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
68
78
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
79
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
69
80
|
|
|
70
81
|
return c.json({
|
|
71
82
|
posts: posts.map((p) => ({
|
|
72
83
|
...p,
|
|
73
84
|
sqid: sqid.encode(p.id),
|
|
74
85
|
mediaAttachments: (mediaMap.get(p.id) ?? []).map((m) =>
|
|
75
|
-
toMediaAttachment(m, r2PublicUrl, imageTransformUrl),
|
|
86
|
+
toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl),
|
|
76
87
|
),
|
|
77
88
|
})),
|
|
78
89
|
|
|
@@ -94,12 +105,13 @@ postsApiRoutes.get("/:id", async (c) => {
|
|
|
94
105
|
const mediaList = await c.var.services.media.getByPostId(post.id);
|
|
95
106
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
96
107
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
108
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
97
109
|
|
|
98
110
|
return c.json({
|
|
99
111
|
...post,
|
|
100
112
|
sqid: sqid.encode(post.id),
|
|
101
113
|
mediaAttachments: mediaList.map((m) =>
|
|
102
|
-
toMediaAttachment(m, r2PublicUrl, imageTransformUrl),
|
|
114
|
+
toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl),
|
|
103
115
|
),
|
|
104
116
|
});
|
|
105
117
|
});
|
|
@@ -157,13 +169,14 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
157
169
|
const mediaList = await c.var.services.media.getByPostId(post.id);
|
|
158
170
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
159
171
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
172
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
160
173
|
|
|
161
174
|
return c.json(
|
|
162
175
|
{
|
|
163
176
|
...post,
|
|
164
177
|
sqid: sqid.encode(post.id),
|
|
165
178
|
mediaAttachments: mediaList.map((m) =>
|
|
166
|
-
toMediaAttachment(m, r2PublicUrl, imageTransformUrl),
|
|
179
|
+
toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl),
|
|
167
180
|
),
|
|
168
181
|
},
|
|
169
182
|
201,
|
|
@@ -233,12 +246,13 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
|
233
246
|
const mediaList = await c.var.services.media.getByPostId(post.id);
|
|
234
247
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
235
248
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
249
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
236
250
|
|
|
237
251
|
return c.json({
|
|
238
252
|
...post,
|
|
239
253
|
sqid: sqid.encode(post.id),
|
|
240
254
|
mediaAttachments: mediaList.map((m) =>
|
|
241
|
-
toMediaAttachment(m, r2PublicUrl, imageTransformUrl),
|
|
255
|
+
toMediaAttachment(m, r2PublicUrl, imageTransformUrl, s3PublicUrl),
|
|
242
256
|
),
|
|
243
257
|
});
|
|
244
258
|
});
|
|
@@ -49,7 +49,13 @@ timelineApiRoutes.get("/", async (c) => {
|
|
|
49
49
|
const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
50
50
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
51
51
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
52
|
-
const
|
|
52
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
53
|
+
const mediaMap = buildMediaMap(
|
|
54
|
+
rawMediaMap,
|
|
55
|
+
r2PublicUrl,
|
|
56
|
+
imageTransformUrl,
|
|
57
|
+
s3PublicUrl,
|
|
58
|
+
);
|
|
53
59
|
|
|
54
60
|
// Get reply counts to identify thread roots
|
|
55
61
|
const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
|
|
@@ -74,6 +80,7 @@ timelineApiRoutes.get("/", async (c) => {
|
|
|
74
80
|
await c.var.services.media.getByPostIds(previewReplyIds),
|
|
75
81
|
r2PublicUrl,
|
|
76
82
|
imageTransformUrl,
|
|
83
|
+
s3PublicUrl,
|
|
77
84
|
)
|
|
78
85
|
: new Map();
|
|
79
86
|
|
package/src/routes/api/upload.ts
CHANGED
|
@@ -11,7 +11,11 @@ import { uuidv7 } from "uuidv7";
|
|
|
11
11
|
import type { Bindings } from "../../types.js";
|
|
12
12
|
import type { AppVariables } from "../../app.js";
|
|
13
13
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getMediaUrl,
|
|
16
|
+
getImageUrl,
|
|
17
|
+
getPublicUrlForProvider,
|
|
18
|
+
} from "../../lib/image.js";
|
|
15
19
|
import { sse, dsSignals } from "../../lib/sse.js";
|
|
16
20
|
|
|
17
21
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
@@ -27,16 +31,16 @@ uploadApiRoutes.use("*", requireAuthApi());
|
|
|
27
31
|
function renderMediaCard(
|
|
28
32
|
media: {
|
|
29
33
|
id: string;
|
|
30
|
-
|
|
34
|
+
storageKey: string;
|
|
31
35
|
mimeType: string;
|
|
32
36
|
originalName: string;
|
|
33
37
|
alt: string | null;
|
|
34
38
|
size: number;
|
|
35
39
|
},
|
|
36
|
-
|
|
40
|
+
publicUrl?: string,
|
|
37
41
|
imageTransformUrl?: string,
|
|
38
42
|
): string {
|
|
39
|
-
const fullUrl = getMediaUrl(media.id, media.
|
|
43
|
+
const fullUrl = getMediaUrl(media.id, media.storageKey, publicUrl);
|
|
40
44
|
const thumbnailUrl = getImageUrl(fullUrl, imageTransformUrl, {
|
|
41
45
|
width: 300,
|
|
42
46
|
quality: 80,
|
|
@@ -116,11 +120,12 @@ function wantsSSE(c: {
|
|
|
116
120
|
|
|
117
121
|
// Upload a file
|
|
118
122
|
uploadApiRoutes.post("/", async (c) => {
|
|
119
|
-
|
|
123
|
+
const storage = c.var.storage;
|
|
124
|
+
if (!storage) {
|
|
120
125
|
if (wantsSSE(c)) {
|
|
121
|
-
return dsSignals({ _uploadError: "
|
|
126
|
+
return dsSignals({ _uploadError: "Storage not configured" });
|
|
122
127
|
}
|
|
123
|
-
return c.json({ error: "
|
|
128
|
+
return c.json({ error: "Storage not configured" }, 500);
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
const formData = await c.req.formData();
|
|
@@ -164,14 +169,12 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
164
169
|
const year = date.getUTCFullYear();
|
|
165
170
|
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
166
171
|
const filename = `${id}.${ext}`;
|
|
167
|
-
const
|
|
172
|
+
const storageKey = `media/${year}/${month}/${filename}`;
|
|
168
173
|
|
|
169
174
|
try {
|
|
170
|
-
// Upload to
|
|
171
|
-
await
|
|
172
|
-
|
|
173
|
-
contentType: file.type,
|
|
174
|
-
},
|
|
175
|
+
// Upload to storage
|
|
176
|
+
await storage.put(storageKey, file.stream(), {
|
|
177
|
+
contentType: file.type,
|
|
175
178
|
});
|
|
176
179
|
|
|
177
180
|
// Save to database
|
|
@@ -181,14 +184,21 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
181
184
|
originalName: file.name,
|
|
182
185
|
mimeType: file.type,
|
|
183
186
|
size: file.size,
|
|
184
|
-
|
|
187
|
+
storageKey,
|
|
188
|
+
provider: c.env.STORAGE_DRIVER || "r2",
|
|
185
189
|
});
|
|
186
190
|
|
|
187
191
|
// SSE response for Datastar
|
|
188
192
|
if (wantsSSE(c)) {
|
|
193
|
+
const provider = c.env.STORAGE_DRIVER || "r2";
|
|
194
|
+
const mediaPublicUrl = getPublicUrlForProvider(
|
|
195
|
+
provider,
|
|
196
|
+
c.env.R2_PUBLIC_URL,
|
|
197
|
+
c.env.S3_PUBLIC_URL,
|
|
198
|
+
);
|
|
189
199
|
const cardHtml = renderMediaCard(
|
|
190
200
|
media,
|
|
191
|
-
|
|
201
|
+
mediaPublicUrl,
|
|
192
202
|
c.env.IMAGE_TRANSFORM_URL,
|
|
193
203
|
);
|
|
194
204
|
|
|
@@ -203,7 +213,13 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
203
213
|
}
|
|
204
214
|
|
|
205
215
|
// JSON response for API clients
|
|
206
|
-
const
|
|
216
|
+
const provider = c.env.STORAGE_DRIVER || "r2";
|
|
217
|
+
const mediaPublicUrl = getPublicUrlForProvider(
|
|
218
|
+
provider,
|
|
219
|
+
c.env.R2_PUBLIC_URL,
|
|
220
|
+
c.env.S3_PUBLIC_URL,
|
|
221
|
+
);
|
|
222
|
+
const publicUrl = getMediaUrl(media.id, storageKey, mediaPublicUrl);
|
|
207
223
|
return c.json({
|
|
208
224
|
id: media.id,
|
|
209
225
|
filename: media.filename,
|
|
@@ -229,12 +245,18 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
229
245
|
uploadApiRoutes.get("/", async (c) => {
|
|
230
246
|
const limit = parseInt(c.req.query("limit") ?? "50", 10);
|
|
231
247
|
const mediaList = await c.var.services.media.list(limit);
|
|
248
|
+
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
249
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
232
250
|
|
|
233
251
|
return c.json({
|
|
234
252
|
media: mediaList.map((m) => ({
|
|
235
253
|
id: m.id,
|
|
236
254
|
filename: m.filename,
|
|
237
|
-
url: getMediaUrl(
|
|
255
|
+
url: getMediaUrl(
|
|
256
|
+
m.id,
|
|
257
|
+
m.storageKey,
|
|
258
|
+
getPublicUrlForProvider(m.provider, r2PublicUrl, s3PublicUrl),
|
|
259
|
+
),
|
|
238
260
|
mimeType: m.mimeType,
|
|
239
261
|
size: m.size,
|
|
240
262
|
createdAt: m.createdAt,
|
|
@@ -250,13 +272,14 @@ uploadApiRoutes.delete("/:id", async (c) => {
|
|
|
250
272
|
return c.json({ error: "Not found" }, 404);
|
|
251
273
|
}
|
|
252
274
|
|
|
253
|
-
// Delete from
|
|
254
|
-
|
|
275
|
+
// Delete from storage
|
|
276
|
+
const storage = c.var.storage;
|
|
277
|
+
if (storage) {
|
|
255
278
|
try {
|
|
256
|
-
await
|
|
279
|
+
await storage.delete(media.storageKey);
|
|
257
280
|
} catch (err) {
|
|
258
281
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
259
|
-
console.error("
|
|
282
|
+
console.error("Storage delete error:", err);
|
|
260
283
|
}
|
|
261
284
|
}
|
|
262
285
|
|
|
@@ -13,7 +13,11 @@ import type { AppVariables } from "../../app.js";
|
|
|
13
13
|
import { DashLayout } from "../../theme/layouts/index.js";
|
|
14
14
|
import { EmptyState, DangerZone } from "../../theme/components/index.js";
|
|
15
15
|
import * as time from "../../lib/time.js";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
getMediaUrl,
|
|
18
|
+
getImageUrl,
|
|
19
|
+
getPublicUrlForProvider,
|
|
20
|
+
} from "../../lib/image.js";
|
|
17
21
|
import { dsRedirect } from "../../lib/sse.js";
|
|
18
22
|
|
|
19
23
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
@@ -36,12 +40,19 @@ function MediaCard({
|
|
|
36
40
|
media,
|
|
37
41
|
r2PublicUrl,
|
|
38
42
|
imageTransformUrl,
|
|
43
|
+
s3PublicUrl,
|
|
39
44
|
}: {
|
|
40
45
|
media: Media;
|
|
41
46
|
r2PublicUrl?: string;
|
|
42
47
|
imageTransformUrl?: string;
|
|
48
|
+
s3PublicUrl?: string;
|
|
43
49
|
}) {
|
|
44
|
-
const
|
|
50
|
+
const publicUrl = getPublicUrlForProvider(
|
|
51
|
+
media.provider,
|
|
52
|
+
r2PublicUrl,
|
|
53
|
+
s3PublicUrl,
|
|
54
|
+
);
|
|
55
|
+
const fullUrl = getMediaUrl(media.id, media.storageKey, publicUrl);
|
|
45
56
|
const thumbnailUrl = getImageUrl(fullUrl, imageTransformUrl, {
|
|
46
57
|
width: 300,
|
|
47
58
|
quality: 80,
|
|
@@ -96,10 +107,12 @@ function MediaListContent({
|
|
|
96
107
|
mediaList,
|
|
97
108
|
r2PublicUrl,
|
|
98
109
|
imageTransformUrl,
|
|
110
|
+
s3PublicUrl,
|
|
99
111
|
}: {
|
|
100
112
|
mediaList: Media[];
|
|
101
113
|
r2PublicUrl?: string;
|
|
102
114
|
imageTransformUrl?: string;
|
|
115
|
+
s3PublicUrl?: string;
|
|
103
116
|
}) {
|
|
104
117
|
const { t } = useLingui();
|
|
105
118
|
|
|
@@ -187,6 +200,7 @@ function MediaListContent({
|
|
|
187
200
|
media={m}
|
|
188
201
|
r2PublicUrl={r2PublicUrl}
|
|
189
202
|
imageTransformUrl={imageTransformUrl}
|
|
203
|
+
s3PublicUrl={s3PublicUrl}
|
|
190
204
|
/>
|
|
191
205
|
))}
|
|
192
206
|
</div>
|
|
@@ -217,13 +231,20 @@ function ViewMediaContent({
|
|
|
217
231
|
media,
|
|
218
232
|
r2PublicUrl,
|
|
219
233
|
imageTransformUrl,
|
|
234
|
+
s3PublicUrl,
|
|
220
235
|
}: {
|
|
221
236
|
media: Media;
|
|
222
237
|
r2PublicUrl?: string;
|
|
223
238
|
imageTransformUrl?: string;
|
|
239
|
+
s3PublicUrl?: string;
|
|
224
240
|
}) {
|
|
225
241
|
const { t } = useLingui();
|
|
226
|
-
const
|
|
242
|
+
const publicUrl = getPublicUrlForProvider(
|
|
243
|
+
media.provider,
|
|
244
|
+
r2PublicUrl,
|
|
245
|
+
s3PublicUrl,
|
|
246
|
+
);
|
|
247
|
+
const url = getMediaUrl(media.id, media.storageKey, publicUrl);
|
|
227
248
|
const thumbnailUrl = getImageUrl(url, imageTransformUrl, {
|
|
228
249
|
width: 600,
|
|
229
250
|
quality: 85,
|
|
@@ -401,6 +422,7 @@ mediaRoutes.get("/", async (c) => {
|
|
|
401
422
|
const siteName = await getSiteName(c);
|
|
402
423
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
403
424
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
425
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
404
426
|
|
|
405
427
|
return c.html(
|
|
406
428
|
<DashLayout
|
|
@@ -413,6 +435,7 @@ mediaRoutes.get("/", async (c) => {
|
|
|
413
435
|
mediaList={mediaList}
|
|
414
436
|
r2PublicUrl={r2PublicUrl}
|
|
415
437
|
imageTransformUrl={imageTransformUrl}
|
|
438
|
+
s3PublicUrl={s3PublicUrl}
|
|
416
439
|
/>
|
|
417
440
|
</DashLayout>,
|
|
418
441
|
);
|
|
@@ -424,6 +447,7 @@ mediaRoutes.get("/picker", async (c) => {
|
|
|
424
447
|
const mediaList = await c.var.services.media.list(100);
|
|
425
448
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
426
449
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
450
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
427
451
|
|
|
428
452
|
if (mediaList.length === 0) {
|
|
429
453
|
return c.html(
|
|
@@ -438,7 +462,12 @@ mediaRoutes.get("/picker", async (c) => {
|
|
|
438
462
|
{mediaList
|
|
439
463
|
.filter((m) => m.mimeType.startsWith("image/"))
|
|
440
464
|
.map((m) => {
|
|
441
|
-
const
|
|
465
|
+
const pUrl = getPublicUrlForProvider(
|
|
466
|
+
m.provider,
|
|
467
|
+
r2PublicUrl,
|
|
468
|
+
s3PublicUrl,
|
|
469
|
+
);
|
|
470
|
+
const url = getMediaUrl(m.id, m.storageKey, pUrl);
|
|
442
471
|
const thumbUrl = getImageUrl(url, imageTransformUrl, {
|
|
443
472
|
width: 150,
|
|
444
473
|
quality: 80,
|
|
@@ -477,6 +506,7 @@ mediaRoutes.get("/:id", async (c) => {
|
|
|
477
506
|
const siteName = await getSiteName(c);
|
|
478
507
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
479
508
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
509
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
480
510
|
|
|
481
511
|
return c.html(
|
|
482
512
|
<DashLayout
|
|
@@ -489,6 +519,7 @@ mediaRoutes.get("/:id", async (c) => {
|
|
|
489
519
|
media={media}
|
|
490
520
|
r2PublicUrl={r2PublicUrl}
|
|
491
521
|
imageTransformUrl={imageTransformUrl}
|
|
522
|
+
s3PublicUrl={s3PublicUrl}
|
|
492
523
|
/>
|
|
493
524
|
</DashLayout>,
|
|
494
525
|
);
|
|
@@ -500,13 +531,14 @@ mediaRoutes.post("/:id/delete", async (c) => {
|
|
|
500
531
|
const media = await c.var.services.media.getById(id);
|
|
501
532
|
if (!media) return c.notFound();
|
|
502
533
|
|
|
503
|
-
// Delete from
|
|
504
|
-
|
|
534
|
+
// Delete from storage
|
|
535
|
+
const storage = c.var.storage;
|
|
536
|
+
if (storage) {
|
|
505
537
|
try {
|
|
506
|
-
await
|
|
538
|
+
await storage.delete(media.storageKey);
|
|
507
539
|
} catch (err) {
|
|
508
540
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
509
|
-
console.error("
|
|
541
|
+
console.error("Storage delete error:", err);
|
|
510
542
|
}
|
|
511
543
|
}
|
|
512
544
|
|
|
@@ -168,6 +168,7 @@ function EditPostContent({
|
|
|
168
168
|
mediaAttachments,
|
|
169
169
|
r2PublicUrl,
|
|
170
170
|
imageTransformUrl,
|
|
171
|
+
s3PublicUrl,
|
|
171
172
|
collections,
|
|
172
173
|
postCollectionIds,
|
|
173
174
|
}: {
|
|
@@ -175,6 +176,7 @@ function EditPostContent({
|
|
|
175
176
|
mediaAttachments: Media[];
|
|
176
177
|
r2PublicUrl?: string;
|
|
177
178
|
imageTransformUrl?: string;
|
|
179
|
+
s3PublicUrl?: string;
|
|
178
180
|
collections: Collection[];
|
|
179
181
|
postCollectionIds: number[];
|
|
180
182
|
}) {
|
|
@@ -190,6 +192,7 @@ function EditPostContent({
|
|
|
190
192
|
mediaAttachments={mediaAttachments}
|
|
191
193
|
r2PublicUrl={r2PublicUrl}
|
|
192
194
|
imageTransformUrl={imageTransformUrl}
|
|
195
|
+
s3PublicUrl={s3PublicUrl}
|
|
193
196
|
collections={collections}
|
|
194
197
|
postCollectionIds={postCollectionIds}
|
|
195
198
|
/>
|
|
@@ -232,6 +235,7 @@ postsRoutes.get("/:id/edit", async (c) => {
|
|
|
232
235
|
const mediaAttachments = await c.var.services.media.getByPostId(post.id);
|
|
233
236
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
234
237
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
238
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
235
239
|
const collections = await c.var.services.collections.list();
|
|
236
240
|
const postCollections =
|
|
237
241
|
await c.var.services.collections.getCollectionsForPost(post.id);
|
|
@@ -249,6 +253,7 @@ postsRoutes.get("/:id/edit", async (c) => {
|
|
|
249
253
|
mediaAttachments={mediaAttachments}
|
|
250
254
|
r2PublicUrl={r2PublicUrl}
|
|
251
255
|
imageTransformUrl={imageTransformUrl}
|
|
256
|
+
s3PublicUrl={s3PublicUrl}
|
|
252
257
|
collections={collections}
|
|
253
258
|
postCollectionIds={postCollectionIds}
|
|
254
259
|
/>
|
package/src/routes/feed/rss.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { Bindings } from "../../types.js";
|
|
|
7
7
|
import type { AppVariables } from "../../app.js";
|
|
8
8
|
import * as sqid from "../../lib/sqid.js";
|
|
9
9
|
import * as time from "../../lib/time.js";
|
|
10
|
-
import { getMediaUrl } from "../../lib/image.js";
|
|
10
|
+
import { getMediaUrl, getPublicUrlForProvider } from "../../lib/image.js";
|
|
11
11
|
|
|
12
12
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
13
13
|
|
|
@@ -20,6 +20,7 @@ rssRoutes.get("/", async (c) => {
|
|
|
20
20
|
const siteDescription = all["SITE_DESCRIPTION"] ?? "";
|
|
21
21
|
const siteUrl = c.env.SITE_URL;
|
|
22
22
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
23
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
23
24
|
|
|
24
25
|
const posts = await c.var.services.posts.list({
|
|
25
26
|
visibility: ["featured", "quiet"],
|
|
@@ -40,7 +41,7 @@ rssRoutes.get("/", async (c) => {
|
|
|
40
41
|
const postMedia = mediaMap.get(post.id);
|
|
41
42
|
const firstMedia = postMedia?.[0];
|
|
42
43
|
const enclosure = firstMedia
|
|
43
|
-
? `\n <enclosure url="${getMediaUrl(firstMedia.id, firstMedia.
|
|
44
|
+
? `\n <enclosure url="${getMediaUrl(firstMedia.id, firstMedia.storageKey, getPublicUrlForProvider(firstMedia.provider, r2PublicUrl, s3PublicUrl))}" length="${firstMedia.size}" type="${firstMedia.mimeType}"/>`
|
|
44
45
|
: "";
|
|
45
46
|
|
|
46
47
|
return `
|
|
@@ -70,7 +70,13 @@ homeRoutes.get("/", async (c) => {
|
|
|
70
70
|
const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
71
71
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
72
72
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
73
|
-
const
|
|
73
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
74
|
+
const mediaMap = buildMediaMap(
|
|
75
|
+
rawMediaMap,
|
|
76
|
+
r2PublicUrl,
|
|
77
|
+
imageTransformUrl,
|
|
78
|
+
s3PublicUrl,
|
|
79
|
+
);
|
|
74
80
|
|
|
75
81
|
// Get reply counts to identify thread roots
|
|
76
82
|
const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
|
|
@@ -95,6 +101,7 @@ homeRoutes.get("/", async (c) => {
|
|
|
95
101
|
await c.var.services.media.getByPostIds(previewReplyIds),
|
|
96
102
|
r2PublicUrl,
|
|
97
103
|
imageTransformUrl,
|
|
104
|
+
s3PublicUrl,
|
|
98
105
|
)
|
|
99
106
|
: new Map();
|
|
100
107
|
|
|
@@ -10,7 +10,11 @@ import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
|
|
|
10
10
|
import { MediaGallery } from "../../theme/components/index.js";
|
|
11
11
|
import * as sqid from "../../lib/sqid.js";
|
|
12
12
|
import * as time from "../../lib/time.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
getMediaUrl,
|
|
15
|
+
getImageUrl,
|
|
16
|
+
getPublicUrlForProvider,
|
|
17
|
+
} from "../../lib/image.js";
|
|
14
18
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
15
19
|
|
|
16
20
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
@@ -87,22 +91,30 @@ postRoutes.get("/:id", async (c) => {
|
|
|
87
91
|
const rawMedia = await c.var.services.media.getByPostId(post.id);
|
|
88
92
|
const r2PublicUrl = c.env.R2_PUBLIC_URL;
|
|
89
93
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
94
|
+
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
95
|
+
|
|
96
|
+
const mediaAttachments: MediaAttachment[] = rawMedia.map((m) => {
|
|
97
|
+
const publicUrl = getPublicUrlForProvider(
|
|
98
|
+
m.provider,
|
|
99
|
+
r2PublicUrl,
|
|
100
|
+
s3PublicUrl,
|
|
101
|
+
);
|
|
102
|
+
return {
|
|
103
|
+
id: m.id,
|
|
104
|
+
url: getMediaUrl(m.id, m.storageKey, publicUrl),
|
|
105
|
+
previewUrl: getImageUrl(
|
|
106
|
+
getMediaUrl(m.id, m.storageKey, publicUrl),
|
|
107
|
+
imageTransformUrl,
|
|
108
|
+
{ width: 400, quality: 80, format: "auto", fit: "cover" },
|
|
109
|
+
),
|
|
110
|
+
alt: m.alt,
|
|
111
|
+
blurhash: m.blurhash,
|
|
112
|
+
width: m.width,
|
|
113
|
+
height: m.height,
|
|
114
|
+
position: m.position,
|
|
115
|
+
mimeType: m.mimeType,
|
|
116
|
+
};
|
|
117
|
+
});
|
|
106
118
|
|
|
107
119
|
const navData = await getNavigationData(c);
|
|
108
120
|
const title = post.title || navData.siteName;
|