koishi-plugin-memesluna 0.4.5 → 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 -75
- package/lib/service.d.ts +2 -8
- 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",
|
|
@@ -213,17 +221,6 @@ var MemesLunaService = class extends import_koishi.Service {
|
|
|
213
221
|
indexes: ["hash"]
|
|
214
222
|
}
|
|
215
223
|
);
|
|
216
|
-
this.ctx.database.extend(
|
|
217
|
-
"memesluna_collection_stats",
|
|
218
|
-
{
|
|
219
|
-
collection: "string",
|
|
220
|
-
api_call_count: "integer",
|
|
221
|
-
updated_at: "timestamp"
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
primary: "collection"
|
|
225
|
-
}
|
|
226
|
-
);
|
|
227
224
|
this.ctx.database.extend(
|
|
228
225
|
"memesluna_staged_images",
|
|
229
226
|
{
|
|
@@ -276,55 +273,42 @@ var MemesLunaService = class extends import_koishi.Service {
|
|
|
276
273
|
return;
|
|
277
274
|
}
|
|
278
275
|
const existingImages = await this.ctx.database.get("memesluna_images", { collection: colName });
|
|
279
|
-
const
|
|
276
|
+
const existingFilenames = new Set(existingImages.map((img) => img.filename));
|
|
280
277
|
const existingExternalValues = new Set(
|
|
281
278
|
existingImages.filter((img) => img.type === "external").map((img) => img.value)
|
|
282
279
|
);
|
|
280
|
+
const existingIndices = new Set(existingImages.map((img) => img.index));
|
|
283
281
|
let maxIndex = existingImages.reduce((max, img) => Math.max(max, img.index), 0);
|
|
284
282
|
for (const filename of files) {
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (isNaN(index) || String(index) !== basename || index < 1 || index > 99999) {
|
|
289
|
-
index = ++maxIndex;
|
|
290
|
-
const newFilename = `${index}${ext}`;
|
|
283
|
+
const safeName = sanitizeFilename(filename);
|
|
284
|
+
let currentName = filename;
|
|
285
|
+
if (safeName !== filename) {
|
|
291
286
|
const oldPath = import_path.default.join(colDir, filename);
|
|
292
|
-
const newPath = import_path.default.join(colDir,
|
|
287
|
+
const newPath = import_path.default.join(colDir, safeName);
|
|
293
288
|
try {
|
|
294
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 {
|
|
295
298
|
await this.ctx.database.create("memesluna_images", {
|
|
296
299
|
id: (0, import_crypto.randomUUID)(),
|
|
297
300
|
collection: colName,
|
|
298
301
|
index,
|
|
299
|
-
filename:
|
|
302
|
+
filename: currentName,
|
|
300
303
|
type: "local",
|
|
301
|
-
value:
|
|
302
|
-
mime: this.getMimeByFilename(
|
|
303
|
-
...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)),
|
|
304
307
|
created_at: /* @__PURE__ */ new Date()
|
|
305
308
|
});
|
|
306
|
-
|
|
309
|
+
existingFilenames.add(currentName);
|
|
307
310
|
} catch {
|
|
308
311
|
}
|
|
309
|
-
} else {
|
|
310
|
-
if (!existingIndices.has(index)) {
|
|
311
|
-
try {
|
|
312
|
-
await this.ctx.database.create("memesluna_images", {
|
|
313
|
-
id: (0, import_crypto.randomUUID)(),
|
|
314
|
-
collection: colName,
|
|
315
|
-
index,
|
|
316
|
-
filename,
|
|
317
|
-
type: "local",
|
|
318
|
-
value: filename,
|
|
319
|
-
mime: this.getMimeByFilename(filename),
|
|
320
|
-
...await this.getImageFingerprints(await import_promises.default.readFile(import_path.default.join(colDir, filename))),
|
|
321
|
-
created_at: /* @__PURE__ */ new Date()
|
|
322
|
-
});
|
|
323
|
-
existingIndices.add(index);
|
|
324
|
-
maxIndex = Math.max(maxIndex, index);
|
|
325
|
-
} catch {
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
312
|
}
|
|
329
313
|
}
|
|
330
314
|
const linksFile = this.getCollectionLinksFile(colName);
|
|
@@ -854,6 +838,19 @@ var MemesLunaService = class extends import_koishi.Service {
|
|
|
854
838
|
}
|
|
855
839
|
}
|
|
856
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
|
+
}
|
|
857
854
|
async addLocalImageBuffer(collectionName, buffer, originalName, extHint) {
|
|
858
855
|
this.ensureCollectionName(collectionName);
|
|
859
856
|
if (!await this.collectionExists(collectionName)) {
|
|
@@ -874,7 +871,9 @@ var MemesLunaService = class extends import_koishi.Service {
|
|
|
874
871
|
if (!IMAGE_EXTENSIONS.has(finalExt)) {
|
|
875
872
|
throw new Error("Unsupported image format");
|
|
876
873
|
}
|
|
877
|
-
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);
|
|
878
877
|
const backend = this.getStorageBackend();
|
|
879
878
|
if (backend) {
|
|
880
879
|
const result = await backend.upload(buffer, finalName);
|
|
@@ -1107,7 +1106,6 @@ var MemesLunaService = class extends import_koishi.Service {
|
|
|
1107
1106
|
}
|
|
1108
1107
|
const createdAt = dates.length ? new Date(Math.min(...dates.map((date) => date.getTime()), statCreatedAt?.getTime() || Infinity)) : statCreatedAt;
|
|
1109
1108
|
const updatedAt = dates.length ? new Date(Math.max(...dates.map((date) => date.getTime()), statUpdatedAt?.getTime() || 0)) : statUpdatedAt;
|
|
1110
|
-
const apiCallCount = await this.getCollectionApiCallCount(collectionName);
|
|
1111
1109
|
return {
|
|
1112
1110
|
name: collectionName,
|
|
1113
1111
|
description,
|
|
@@ -1117,36 +1115,9 @@ var MemesLunaService = class extends import_koishi.Service {
|
|
|
1117
1115
|
hasContent: localImages.length > 0 || links.length > 0,
|
|
1118
1116
|
createdAt,
|
|
1119
1117
|
updatedAt,
|
|
1120
|
-
apiCallCount,
|
|
1121
1118
|
cover: localImages[0]
|
|
1122
1119
|
};
|
|
1123
1120
|
}
|
|
1124
|
-
async getCollectionApiCallCount(collectionName) {
|
|
1125
|
-
if (!this.isValidCollectionName(collectionName)) {
|
|
1126
|
-
return 0;
|
|
1127
|
-
}
|
|
1128
|
-
const rows = await this.ctx.database.get("memesluna_collection_stats", { collection: collectionName });
|
|
1129
|
-
return rows[0]?.api_call_count || 0;
|
|
1130
|
-
}
|
|
1131
|
-
async incrementCollectionApiCallCount(collectionName) {
|
|
1132
|
-
if (!this.isValidCollectionName(collectionName)) {
|
|
1133
|
-
return;
|
|
1134
|
-
}
|
|
1135
|
-
const rows = await this.ctx.database.get("memesluna_collection_stats", { collection: collectionName });
|
|
1136
|
-
const now = /* @__PURE__ */ new Date();
|
|
1137
|
-
if (rows.length) {
|
|
1138
|
-
await this.ctx.database.set("memesluna_collection_stats", { collection: collectionName }, {
|
|
1139
|
-
api_call_count: (rows[0].api_call_count || 0) + 1,
|
|
1140
|
-
updated_at: now
|
|
1141
|
-
});
|
|
1142
|
-
return;
|
|
1143
|
-
}
|
|
1144
|
-
await this.ctx.database.create("memesluna_collection_stats", {
|
|
1145
|
-
collection: collectionName,
|
|
1146
|
-
api_call_count: 1,
|
|
1147
|
-
updated_at: now
|
|
1148
|
-
});
|
|
1149
|
-
}
|
|
1150
1121
|
async getRandomResource(collectionName) {
|
|
1151
1122
|
if (!this.isValidCollectionName(collectionName)) {
|
|
1152
1123
|
return null;
|
|
@@ -1366,7 +1337,6 @@ async function applyDynamicForward(ctx, config, service, routeName, _query, requ
|
|
|
1366
1337
|
if (!resource) {
|
|
1367
1338
|
return { notFound: true };
|
|
1368
1339
|
}
|
|
1369
|
-
await service.incrementCollectionApiCallCount(routeName);
|
|
1370
1340
|
if (resource.type === "external") {
|
|
1371
1341
|
return { redirectTo: resource.value };
|
|
1372
1342
|
}
|
|
@@ -1988,6 +1958,24 @@ function applyServer(ctx, config, service) {
|
|
|
1988
1958
|
);
|
|
1989
1959
|
setKoaResponse(koa, result);
|
|
1990
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
|
+
});
|
|
1991
1979
|
}
|
|
1992
1980
|
__name(applyServer, "applyServer");
|
|
1993
1981
|
function apply(ctx, config) {
|
|
@@ -2122,5 +2110,6 @@ var inject = {
|
|
|
2122
2110
|
hashImageBuffer,
|
|
2123
2111
|
inject,
|
|
2124
2112
|
isReservedPath,
|
|
2125
|
-
name
|
|
2113
|
+
name,
|
|
2114
|
+
sanitizeFilename
|
|
2126
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 {
|
|
@@ -28,7 +29,6 @@ export interface CollectionInfo {
|
|
|
28
29
|
hasContent: boolean;
|
|
29
30
|
createdAt?: Date;
|
|
30
31
|
updatedAt?: Date;
|
|
31
|
-
apiCallCount: number;
|
|
32
32
|
cover?: string;
|
|
33
33
|
}
|
|
34
34
|
export interface CollectionResource {
|
|
@@ -118,6 +118,7 @@ export declare class MemesLunaService extends Service {
|
|
|
118
118
|
private buildStagedFilename;
|
|
119
119
|
private resolveStagedImagePath;
|
|
120
120
|
private deduplicateFilename;
|
|
121
|
+
private deduplicateDatabaseFilename;
|
|
121
122
|
addLocalImageBuffer(collectionName: string, buffer: Buffer, originalName?: string, extHint?: string): Promise<string>;
|
|
122
123
|
addLocalImageBase64(collectionName: string, base64Data: string, originalName?: string): Promise<string>;
|
|
123
124
|
addStagedImageBuffer(buffer: Buffer, originalName?: string, source?: string, reason?: string): Promise<StagedImageInfo>;
|
|
@@ -133,8 +134,6 @@ export declare class MemesLunaService extends Service {
|
|
|
133
134
|
deleteImageFromCollection(collectionName: string, filename: string): Promise<boolean>;
|
|
134
135
|
moveImageToCollection(sourceCollection: string, targetCollection: string, filename: string): Promise<string | null>;
|
|
135
136
|
getCollectionInfo(collectionName: string): Promise<CollectionInfo | null>;
|
|
136
|
-
getCollectionApiCallCount(collectionName: string): Promise<number>;
|
|
137
|
-
incrementCollectionApiCallCount(collectionName: string): Promise<void>;
|
|
138
137
|
getRandomResource(collectionName: string): Promise<CollectionResource | null>;
|
|
139
138
|
private mapEndpoint;
|
|
140
139
|
getEndpoints(): Promise<ApiEndpoint[]>;
|
|
@@ -177,11 +176,6 @@ declare module 'koishi' {
|
|
|
177
176
|
perceptual_hash: string;
|
|
178
177
|
created_at: Date;
|
|
179
178
|
};
|
|
180
|
-
memesluna_collection_stats: {
|
|
181
|
-
collection: string;
|
|
182
|
-
api_call_count: number;
|
|
183
|
-
updated_at: Date;
|
|
184
|
-
};
|
|
185
179
|
memesluna_staged_images: {
|
|
186
180
|
id: string;
|
|
187
181
|
filename: string;
|
package/package.json
CHANGED