koishi-plugin-memesluna 0.4.6 → 0.4.7
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/lib/index.js +64 -35
- package/lib/service.d.ts +2 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -36,7 +36,8 @@ __export(index_exports, {
|
|
|
36
36
|
hashImageBuffer: () => hashImageBuffer,
|
|
37
37
|
inject: () => inject,
|
|
38
38
|
isReservedPath: () => isReservedPath,
|
|
39
|
-
name: () => name
|
|
39
|
+
name: () => name,
|
|
40
|
+
sanitizeFilename: () => sanitizeFilename
|
|
40
41
|
});
|
|
41
42
|
module.exports = __toCommonJS(index_exports);
|
|
42
43
|
var import_fs = require("fs");
|
|
@@ -48,6 +49,13 @@ var import_crypto = require("crypto");
|
|
|
48
49
|
var import_promises = __toESM(require("fs/promises"));
|
|
49
50
|
var import_path = __toESM(require("path"));
|
|
50
51
|
var import_koishi = require("koishi");
|
|
52
|
+
function sanitizeFilename(filename) {
|
|
53
|
+
const ext = import_path.default.extname(filename).toLowerCase();
|
|
54
|
+
const base = import_path.default.basename(filename, ext);
|
|
55
|
+
const safeBase = base.replace(/[\s/\\?%*:|"<>,;=@]/g, "_");
|
|
56
|
+
return `${safeBase}${ext}`;
|
|
57
|
+
}
|
|
58
|
+
__name(sanitizeFilename, "sanitizeFilename");
|
|
51
59
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
52
60
|
".jpg",
|
|
53
61
|
".jpeg",
|
|
@@ -265,55 +273,42 @@ var MemesLunaService = class extends import_koishi.Service {
|
|
|
265
273
|
return;
|
|
266
274
|
}
|
|
267
275
|
const existingImages = await this.ctx.database.get("memesluna_images", { collection: colName });
|
|
268
|
-
const
|
|
276
|
+
const existingFilenames = new Set(existingImages.map((img) => img.filename));
|
|
269
277
|
const existingExternalValues = new Set(
|
|
270
278
|
existingImages.filter((img) => img.type === "external").map((img) => img.value)
|
|
271
279
|
);
|
|
280
|
+
const existingIndices = new Set(existingImages.map((img) => img.index));
|
|
272
281
|
let maxIndex = existingImages.reduce((max, img) => Math.max(max, img.index), 0);
|
|
273
282
|
for (const filename of files) {
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (isNaN(index) || String(index) !== basename || index < 1 || index > 99999) {
|
|
278
|
-
index = ++maxIndex;
|
|
279
|
-
const newFilename = `${index}${ext}`;
|
|
283
|
+
const safeName = sanitizeFilename(filename);
|
|
284
|
+
let currentName = filename;
|
|
285
|
+
if (safeName !== filename) {
|
|
280
286
|
const oldPath = import_path.default.join(colDir, filename);
|
|
281
|
-
const newPath = import_path.default.join(colDir,
|
|
287
|
+
const newPath = import_path.default.join(colDir, safeName);
|
|
282
288
|
try {
|
|
283
289
|
await import_promises.default.rename(oldPath, newPath);
|
|
290
|
+
currentName = safeName;
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (!existingFilenames.has(currentName)) {
|
|
295
|
+
const index = ++maxIndex;
|
|
296
|
+
const fullPath = import_path.default.join(colDir, currentName);
|
|
297
|
+
try {
|
|
284
298
|
await this.ctx.database.create("memesluna_images", {
|
|
285
299
|
id: (0, import_crypto.randomUUID)(),
|
|
286
300
|
collection: colName,
|
|
287
301
|
index,
|
|
288
|
-
filename:
|
|
302
|
+
filename: currentName,
|
|
289
303
|
type: "local",
|
|
290
|
-
value:
|
|
291
|
-
mime: this.getMimeByFilename(
|
|
292
|
-
...await this.getImageFingerprints(await import_promises.default.readFile(
|
|
304
|
+
value: currentName,
|
|
305
|
+
mime: this.getMimeByFilename(currentName),
|
|
306
|
+
...await this.getImageFingerprints(await import_promises.default.readFile(fullPath)),
|
|
293
307
|
created_at: /* @__PURE__ */ new Date()
|
|
294
308
|
});
|
|
295
|
-
|
|
309
|
+
existingFilenames.add(currentName);
|
|
296
310
|
} catch {
|
|
297
311
|
}
|
|
298
|
-
} else {
|
|
299
|
-
if (!existingIndices.has(index)) {
|
|
300
|
-
try {
|
|
301
|
-
await this.ctx.database.create("memesluna_images", {
|
|
302
|
-
id: (0, import_crypto.randomUUID)(),
|
|
303
|
-
collection: colName,
|
|
304
|
-
index,
|
|
305
|
-
filename,
|
|
306
|
-
type: "local",
|
|
307
|
-
value: filename,
|
|
308
|
-
mime: this.getMimeByFilename(filename),
|
|
309
|
-
...await this.getImageFingerprints(await import_promises.default.readFile(import_path.default.join(colDir, filename))),
|
|
310
|
-
created_at: /* @__PURE__ */ new Date()
|
|
311
|
-
});
|
|
312
|
-
existingIndices.add(index);
|
|
313
|
-
maxIndex = Math.max(maxIndex, index);
|
|
314
|
-
} catch {
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
312
|
}
|
|
318
313
|
}
|
|
319
314
|
const linksFile = this.getCollectionLinksFile(colName);
|
|
@@ -843,6 +838,19 @@ var MemesLunaService = class extends import_koishi.Service {
|
|
|
843
838
|
}
|
|
844
839
|
}
|
|
845
840
|
}
|
|
841
|
+
async deduplicateDatabaseFilename(collectionName, filename) {
|
|
842
|
+
const parsed = import_path.default.parse(filename);
|
|
843
|
+
let counter = 1;
|
|
844
|
+
let candidate = filename;
|
|
845
|
+
while (true) {
|
|
846
|
+
const rows = await this.ctx.database.get("memesluna_images", { collection: collectionName, filename: candidate });
|
|
847
|
+
if (!rows.length) {
|
|
848
|
+
return candidate;
|
|
849
|
+
}
|
|
850
|
+
candidate = `${parsed.name}_${counter}${parsed.ext}`;
|
|
851
|
+
counter++;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
846
854
|
async addLocalImageBuffer(collectionName, buffer, originalName, extHint) {
|
|
847
855
|
this.ensureCollectionName(collectionName);
|
|
848
856
|
if (!await this.collectionExists(collectionName)) {
|
|
@@ -863,7 +871,9 @@ var MemesLunaService = class extends import_koishi.Service {
|
|
|
863
871
|
if (!IMAGE_EXTENSIONS.has(finalExt)) {
|
|
864
872
|
throw new Error("Unsupported image format");
|
|
865
873
|
}
|
|
866
|
-
const
|
|
874
|
+
const baseName = originalName ? import_path.default.basename(originalName, import_path.default.extname(originalName)) : `image-${Date.now()}`;
|
|
875
|
+
const safeBase = sanitizeFilename(`${baseName}${finalExt}`);
|
|
876
|
+
const finalName = await this.deduplicateDatabaseFilename(collectionName, safeBase);
|
|
867
877
|
const backend = this.getStorageBackend();
|
|
868
878
|
if (backend) {
|
|
869
879
|
const result = await backend.upload(buffer, finalName);
|
|
@@ -1948,6 +1958,24 @@ function applyServer(ctx, config, service) {
|
|
|
1948
1958
|
);
|
|
1949
1959
|
setKoaResponse(koa, result);
|
|
1950
1960
|
});
|
|
1961
|
+
ctx.server.get(`${basePath}/:name/:filename`, async (koa) => {
|
|
1962
|
+
const collectionName = toTrimmedString(koa.params.name);
|
|
1963
|
+
const filename = toTrimmedString(koa.params.filename);
|
|
1964
|
+
if (isReservedPath(collectionName)) {
|
|
1965
|
+
koa.status = 404;
|
|
1966
|
+
koa.body = { error: "Not Found" };
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
const image = await service.getLocalImageBuffer(collectionName, filename);
|
|
1970
|
+
if (!image) {
|
|
1971
|
+
koa.status = 404;
|
|
1972
|
+
koa.body = { error: "Image not found" };
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
koa.status = 200;
|
|
1976
|
+
koa.set("Content-Type", image.mime);
|
|
1977
|
+
koa.body = image.buffer;
|
|
1978
|
+
});
|
|
1951
1979
|
}
|
|
1952
1980
|
__name(applyServer, "applyServer");
|
|
1953
1981
|
function apply(ctx, config) {
|
|
@@ -2082,5 +2110,6 @@ var inject = {
|
|
|
2082
2110
|
hashImageBuffer,
|
|
2083
2111
|
inject,
|
|
2084
2112
|
isReservedPath,
|
|
2085
|
-
name
|
|
2113
|
+
name,
|
|
2114
|
+
sanitizeFilename
|
|
2086
2115
|
});
|
package/lib/service.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Context, Service } from 'koishi';
|
|
2
2
|
import type { Config } from './config';
|
|
3
|
+
export declare function sanitizeFilename(filename: string): string;
|
|
3
4
|
export declare function hashImageBuffer(buffer: Buffer): string;
|
|
4
5
|
export declare function isReservedPath(name: string): boolean;
|
|
5
6
|
export interface ApiEndpoint {
|
|
@@ -117,6 +118,7 @@ export declare class MemesLunaService extends Service {
|
|
|
117
118
|
private buildStagedFilename;
|
|
118
119
|
private resolveStagedImagePath;
|
|
119
120
|
private deduplicateFilename;
|
|
121
|
+
private deduplicateDatabaseFilename;
|
|
120
122
|
addLocalImageBuffer(collectionName: string, buffer: Buffer, originalName?: string, extHint?: string): Promise<string>;
|
|
121
123
|
addLocalImageBase64(collectionName: string, base64Data: string, originalName?: string): Promise<string>;
|
|
122
124
|
addStagedImageBuffer(buffer: Buffer, originalName?: string, source?: string, reason?: string): Promise<StagedImageInfo>;
|
package/package.json
CHANGED