@yrpri/api 9.0.90 → 9.0.91

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.
@@ -16,11 +16,50 @@ const disableFlux = false;
16
16
  export class CollectionImageGenerator {
17
17
  async resizeImage(imagePath, width, height) {
18
18
  const resizedImageFilePath = path.join("/tmp", `${uuidv4()}.png`);
19
- await sharp(imagePath)
20
- .resize({ width, height })
21
- .toFile(resizedImageFilePath);
22
- fs.unlinkSync(imagePath);
23
- return resizedImageFilePath;
19
+ try {
20
+ // 1) Initialize Sharp instance
21
+ const image = sharp(imagePath).rotate(); // rotate fixes orientation from EXIF
22
+ // 2) Read metadata to validate format
23
+ const metadata = await image.metadata();
24
+ const validFormats = [
25
+ "jpeg",
26
+ "png",
27
+ "webp",
28
+ "gif",
29
+ "tiff",
30
+ "avif",
31
+ "svg"
32
+ ];
33
+ if (!metadata.format || !validFormats.includes(metadata.format)) {
34
+ throw new Error(`Unsupported format: ${metadata.format} (expected one of ${validFormats.join(", ")})`);
35
+ }
36
+ // 3) Resize + convert
37
+ await image
38
+ .resize({
39
+ width,
40
+ height,
41
+ fit: "inside",
42
+ withoutEnlargement: true, // ensures you won't upscale smaller images
43
+ })
44
+ .toFormat("png", {
45
+ quality: 90,
46
+ progressive: true,
47
+ })
48
+ .toFile(resizedImageFilePath);
49
+ // 4) Remove the original file after successful resize
50
+ fs.unlinkSync(imagePath);
51
+ return resizedImageFilePath;
52
+ }
53
+ catch (err) {
54
+ // Cleanup if something goes wrong
55
+ console.error("Error resizing image:", err);
56
+ // Optionally remove partial or empty output file if it exists
57
+ if (fs.existsSync(resizedImageFilePath)) {
58
+ fs.unlinkSync(resizedImageFilePath);
59
+ }
60
+ // Rethrow or handle error further
61
+ throw err;
62
+ }
24
63
  }
25
64
  async downloadImage(imageUrl, imageFilePath) {
26
65
  const response = await axios({
@@ -222,26 +222,71 @@ router.post("/:imageId/comment", auth.isLoggedInNoAnonymousCheck, auth.can("view
222
222
  router.post("/", isAuthenticated, async function (req, res) {
223
223
  try {
224
224
  const s3 = new aws.S3();
225
- //TODO: Look into making animated gifs work through sharp
226
- const isGif = req.file && req.file.originalname.toLowerCase().indexOf(".gif");
225
+ // 1) Check if the file name ends with ".gif"
226
+ const isGifFilename = (filename) => {
227
+ const lowerCaseFilename = filename.toLowerCase();
228
+ console.log("filename===========>", filename);
229
+ console.log("lowerCaseFilename===========>", lowerCaseFilename);
230
+ return lowerCaseFilename.endsWith(".gif");
231
+ };
232
+ // 2) Create the storage with a Key callback that uses 'isGifFilename'
227
233
  const storage = s3Storage({
228
234
  Key: (req, file, cb) => {
229
235
  crypto.pseudoRandomBytes(16, (err, raw) => {
230
- cb(err, err ? undefined : `${raw.toString("hex")}.${isGif ? "gif" : "png"}`);
236
+ if (err)
237
+ return cb(err);
238
+ const rawHex = raw.toString("hex");
239
+ console.log("file.originalname", file.originalname);
240
+ const isGif = isGifFilename(file.originalname);
241
+ console.log("gif===========>", isGif);
242
+ file.outputFormat = isGif ? "gif" : "png";
243
+ cb(null, `${rawHex}.${isGif ? "gif" : "png"}`);
231
244
  });
232
245
  },
233
246
  s3,
234
247
  Bucket: process.env.S3_BUCKET,
235
248
  multiple: true,
236
249
  resize: models.Image.getSharpVersions(req.query.itemType),
237
- toFormat: isGif ? "gif" : "png",
250
+ toFormat: "png",
238
251
  });
239
- const upload = multer({ storage });
252
+ // 3) Allowed MIME types that Sharp commonly supports
253
+ // (Adjust or expand as needed)
254
+ const allowedMimeTypes = [
255
+ "image/png",
256
+ "image/jpeg",
257
+ "image/jpg",
258
+ "image/gif",
259
+ "image/webp",
260
+ "image/tiff",
261
+ "image/svg+xml"
262
+ ];
263
+ // 4) A fileFilter to reject non-image or invalid Sharp formats
264
+ const fileFilter = (req, file, cb) => {
265
+ console.log("file------------>", file);
266
+ // Check that it's an image and specifically in our allowed list
267
+ if (!allowedMimeTypes.includes(file.mimetype.toLowerCase())) {
268
+ return cb(new Error(`Unsupported file type: ${file.mimetype}. Allowed: ${allowedMimeTypes.join(", ")}`));
269
+ }
270
+ cb(null, true);
271
+ };
272
+ // 5) Construct multer with:
273
+ // - the custom S3-based storage
274
+ // - our fileFilter for validation
275
+ // - a file size limit of 50MB (you can adjust as needed)
276
+ const upload = multer({
277
+ storage,
278
+ fileFilter,
279
+ limits: { fileSize: 50 * 1024 * 1024 } // 50MB limit
280
+ });
281
+ // 6) Use upload.single, handle the success/error callback carefully
240
282
  upload.single("file")(req, res, async function (error) {
241
283
  if (error) {
242
- sendError(res, req.file ? req.file.originalname : "unknown filename", "create", res.user, error);
284
+ // Multer will throw if file is too large or invalid
285
+ console.error("File upload error:", error);
286
+ return res.status(400).json({ error: error.message });
243
287
  }
244
- else {
288
+ // Continue if there's a valid image file
289
+ try {
245
290
  const formats = JSON.stringify(models.Image.createFormatsFromSharpFile(req.file));
246
291
  const image = models.Image.build({
247
292
  user_id: req.user.id,
@@ -257,12 +302,21 @@ router.post("/", isAuthenticated, async function (req, res) {
257
302
  context: "create",
258
303
  userId: req.user ? req.user.id : -1,
259
304
  });
260
- res.send(image);
305
+ return res.send(image);
306
+ }
307
+ catch (err) {
308
+ console.error("Error saving image record:", err);
309
+ return res.status(500).json({ error: "Failed to save image record" });
261
310
  }
262
311
  });
263
312
  }
264
- catch (error) {
265
- sendError(res, req.file.originalname, "create", res.user, error);
313
+ catch (err) {
314
+ console.error("Unexpected error:", err);
315
+ // If req.file exists, use its data; otherwise fallback
316
+ const fileName = req.file ? req.file.originalname : "unknown filename";
317
+ return res.status(500).json({
318
+ error: `Unhandled error while processing '${fileName}'`,
319
+ });
266
320
  }
267
321
  });
268
322
  // Post User Images
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yrpri/api",
3
- "version": "9.0.90",
3
+ "version": "9.0.91",
4
4
  "license": "MIT",
5
5
  "author": "Robert Bjarnason & Citizens Foundation",
6
6
  "repository": {
@@ -94,7 +94,7 @@
94
94
  "sanitize-filename": "^1.6.3",
95
95
  "sequelize": "^6.36.0",
96
96
  "sequelize-cli": "^6.2.0",
97
- "sharp": "^0.33.2",
97
+ "sharp": "^0.33.5",
98
98
  "sitemap": "^7.1.2",
99
99
  "socket.io": "^4.0.0",
100
100
  "stripe": "^17.3.0",