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 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 existingIndices = new Set(existingImages.map((img) => img.index));
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 ext = import_path.default.extname(filename).toLowerCase();
275
- const basename = import_path.default.basename(filename, ext);
276
- let index = parseInt(basename, 10);
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, newFilename);
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: newFilename,
302
+ filename: currentName,
289
303
  type: "local",
290
- value: newFilename,
291
- mime: this.getMimeByFilename(newFilename),
292
- ...await this.getImageFingerprints(await import_promises.default.readFile(newPath)),
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
- existingIndices.add(index);
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 finalName = `${index}${finalExt}`;
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-memesluna",
3
3
  "description": "Image Forward service for Koishi with ChatLuna integration",
4
- "version": "0.4.6",
4
+ "version": "0.4.7",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "types": "lib/index.d.ts",