@muhgholy/next-drive 4.5.0 → 4.6.0

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/README.md CHANGED
@@ -55,17 +55,6 @@ This package uses [subpath exports](https://nodejs.org/api/packages.html#subpath
55
55
  }
56
56
  ```
57
57
 
58
- **For projects using bundlers (Vite, Webpack, etc.):**
59
-
60
- ```json
61
- {
62
- "compilerOptions": {
63
- "module": "esnext",
64
- "moduleResolution": "bundler"
65
- }
66
- }
67
- ```
68
-
69
58
  > ⚠️ The legacy `"moduleResolution": "node"` is **not supported** and will cause build errors with subpath imports like `@muhgholy/next-drive/server`.
70
59
 
71
60
  **FFmpeg** (for video thumbnails):
@@ -583,25 +572,122 @@ GOOGLE_REDIRECT_URI=http://localhost:3000/api/drive?action=callback
583
572
 
584
573
  ---
585
574
 
586
- ## API Endpoints
587
-
588
- All operations use `?action=` query parameter:
589
-
590
- | Action | Method | Description |
591
- | ----------------- | ------ | -------------------- |
592
- | `upload` | POST | Chunked file upload |
593
- | `list` | GET | List folder contents |
594
- | `serve` | GET | Serve file content |
595
- | `thumbnail` | GET | Get file thumbnail |
596
- | `rename` | PATCH | Rename file/folder |
597
- | `trash` | POST | Move to trash |
598
- | `deletePermanent` | DELETE | Delete permanently |
599
- | `restore` | POST | Restore from trash |
600
- | `emptyTrash` | DELETE | Empty all trash |
601
- | `createFolder` | POST | Create folder |
602
- | `move` | POST | Move to new parent |
603
- | `search` | GET | Search by name |
604
- | `quota` | GET | Get storage usage |
575
+ ## Image Optimization
576
+
577
+ Serve optimized images with dynamic compression, resizing, and format conversion using query parameters.
578
+
579
+ ### URL Format
580
+
581
+ ```
582
+ /api/drive?action=serve&id={fileId}&quality={preset}&display={context}&size={preset}&format={format}
583
+ ```
584
+
585
+ ### Parameters
586
+
587
+ | Parameter | Type | Description |
588
+ |-----------|------|-------------|
589
+ | `quality` | `low` / `medium` / `high` / `1-100` | Compression level |
590
+ | `display` | string | Context-based quality adjustment |
591
+ | `size` | string | Predefined dimensions preset |
592
+ | `format` | `jpeg` / `webp` / `avif` / `png` | Output format |
593
+
594
+ ### Quality Presets
595
+
596
+ | Preset | Base Quality | Use Case |
597
+ |--------|--------------|----------|
598
+ | `low` | 30 | Thumbnails, previews |
599
+ | `medium` | 50 | General content |
600
+ | `high` | 75 | High-quality display |
601
+ | `1-100` | Custom | Fine-tuned control |
602
+
603
+ > Quality is dynamically adjusted based on file size. Larger files get more aggressive compression.
604
+
605
+ ### Display Presets (Quality Context)
606
+
607
+ | Display | Quality Factor | Use Case |
608
+ |---------|----------------|----------|
609
+ | `article-header` | 0.9 | Hero/banner images |
610
+ | `article-image` | 0.85 | In-content images |
611
+ | `thumbnail` | 0.7 | Small previews |
612
+ | `avatar` | 0.8 | Profile pictures |
613
+ | `logo` | 0.95 | Branding/logos |
614
+ | `card` | 0.8 | Card components |
615
+ | `gallery` | 0.85 | Gallery/grid |
616
+ | `og` | 0.9 | Open Graph/social |
617
+ | `icon` | 0.75 | Small icons |
618
+ | `cover` | 0.9 | Full-width covers |
619
+ | `story` | 0.85 | Story/vertical |
620
+
621
+ ### Size Presets (Dimensions)
622
+
623
+ **Square Sizes:**
624
+ | Size | Dimensions |
625
+ |------|------------|
626
+ | `xs` | 64×64 |
627
+ | `sm` | 128×128 |
628
+ | `md` | 256×256 |
629
+ | `lg` | 512×512 |
630
+ | `xl` | 1024×1024 |
631
+ | `2xl` | 1600×1600 |
632
+ | `icon` | 48×48 |
633
+ | `thumb` | 150×150 |
634
+ | `square` | 600×600 |
635
+ | `avatar-sm` | 64×64 |
636
+ | `avatar-md` | 128×128 |
637
+ | `avatar-lg` | 256×256 |
638
+
639
+ **Landscape (16:9):**
640
+ | Size | Dimensions |
641
+ |------|------------|
642
+ | `landscape-sm` | 480×270 |
643
+ | `landscape` | 800×450 |
644
+ | `landscape-lg` | 1280×720 |
645
+ | `landscape-xl` | 1920×1080 |
646
+
647
+ **Portrait (9:16):**
648
+ | Size | Dimensions |
649
+ |------|------------|
650
+ | `portrait-sm` | 270×480 |
651
+ | `portrait` | 450×800 |
652
+ | `portrait-lg` | 720×1280 |
653
+
654
+ **Wide/Banner:**
655
+ | Size | Dimensions | Ratio |
656
+ |------|------------|-------|
657
+ | `wide` | 1200×630 | OG standard |
658
+ | `banner` | 1200×400 | 3:1 |
659
+ | `banner-sm` | 800×200 | 4:1 |
660
+
661
+ **Other:**
662
+ | Size | Dimensions | Ratio |
663
+ |------|------------|-------|
664
+ | `photo-4x3` | 800×600 | 4:3 |
665
+ | `photo-3x2` | 900×600 | 3:2 |
666
+ | `story` | 1080×1920 | 9:16 |
667
+ | `video` | 1280×720 | 16:9 |
668
+ | `video-sm` | 640×360 | 16:9 |
669
+ | `card-sm` | 300×200 | 3:2 |
670
+ | `card` | 400×300 | 4:3 |
671
+ | `card-lg` | 600×400 | 3:2 |
672
+
673
+ ### Examples
674
+
675
+ ```html
676
+ <!-- Article header with OG dimensions -->
677
+ <img src="/api/drive?action=serve&id=123&display=article-header&size=wide&format=webp">
678
+
679
+ <!-- Thumbnail with aggressive compression -->
680
+ <img src="/api/drive?action=serve&id=123&display=thumbnail&size=thumb&format=webp">
681
+
682
+ <!-- Avatar -->
683
+ <img src="/api/drive?action=serve&id=123&display=avatar&size=avatar-md&format=webp">
684
+
685
+ <!-- Gallery image -->
686
+ <img src="/api/drive?action=serve&id=123&display=gallery&size=landscape&format=webp">
687
+
688
+ <!-- Just quality, no resize -->
689
+ <img src="/api/drive?action=serve&id=123&quality=medium&format=webp">
690
+ ```
605
691
 
606
692
  ---
607
693
 
@@ -369,13 +369,120 @@ var extractImageMetadata = async (filePath) => {
369
369
  return null;
370
370
  }
371
371
  };
372
- var parseQuality = (q) => {
373
- if (!q) return 80;
374
- if (q === "low") return 40;
375
- if (q === "medium") return 60;
376
- if (q === "high") return 80;
377
- const n = parseInt(q, 10);
378
- return isNaN(n) ? 80 : Math.min(100, Math.max(1, n));
372
+ var DISPLAY_PRESETS = {
373
+ "article-header": 0.9,
374
+ // Hero/banner images - high quality
375
+ "article-image": 0.85,
376
+ // In-content images
377
+ "thumbnail": 0.7,
378
+ // Small previews - lower quality ok
379
+ "avatar": 0.8,
380
+ // Profile pictures
381
+ "logo": 0.95,
382
+ // Branding - needs clarity
383
+ "card": 0.8,
384
+ // Card components
385
+ "gallery": 0.85,
386
+ // Gallery/grid images
387
+ "og": 0.9,
388
+ // Open Graph/social sharing
389
+ "icon": 0.75,
390
+ // Small icons
391
+ "cover": 0.9,
392
+ // Full-width covers
393
+ "story": 0.85
394
+ // Story/vertical format
395
+ };
396
+ var SIZE_PRESETS = {
397
+ // Square sizes
398
+ "xs": { width: 64, height: 64 },
399
+ "sm": { width: 128, height: 128 },
400
+ "md": { width: 256, height: 256 },
401
+ "lg": { width: 512, height: 512 },
402
+ "xl": { width: 1024, height: 1024 },
403
+ "2xl": { width: 1600, height: 1600 },
404
+ // Named squares
405
+ "icon": { width: 48, height: 48 },
406
+ "thumb": { width: 150, height: 150 },
407
+ "square": { width: 600, height: 600 },
408
+ "avatar-sm": { width: 64, height: 64 },
409
+ "avatar-md": { width: 128, height: 128 },
410
+ "avatar-lg": { width: 256, height: 256 },
411
+ // Landscape (16:9)
412
+ "landscape-sm": { width: 480, height: 270 },
413
+ "landscape": { width: 800, height: 450 },
414
+ "landscape-lg": { width: 1280, height: 720 },
415
+ "landscape-xl": { width: 1920, height: 1080 },
416
+ // Portrait (9:16)
417
+ "portrait-sm": { width: 270, height: 480 },
418
+ "portrait": { width: 450, height: 800 },
419
+ "portrait-lg": { width: 720, height: 1280 },
420
+ // Wide/Banner (OG, social)
421
+ "wide": { width: 1200, height: 630 },
422
+ // Open Graph standard
423
+ "banner": { width: 1200, height: 400 },
424
+ // Banner/header
425
+ "banner-sm": { width: 800, height: 200 },
426
+ // Classic photo ratios
427
+ "photo-4x3": { width: 800, height: 600 },
428
+ // 4:3
429
+ "photo-3x2": { width: 900, height: 600 },
430
+ // 3:2
431
+ // Story/vertical (9:16)
432
+ "story": { width: 1080, height: 1920 },
433
+ // Video thumbnails
434
+ "video": { width: 1280, height: 720 },
435
+ "video-sm": { width: 640, height: 360 },
436
+ // Card sizes
437
+ "card-sm": { width: 300, height: 200 },
438
+ "card": { width: 400, height: 300 },
439
+ "card-lg": { width: 600, height: 400 }
440
+ };
441
+ var getImageSettings = (fileSizeInBytes, qualityPreset, display, size) => {
442
+ let baseQuality = 80;
443
+ if (qualityPreset === "low") baseQuality = 30;
444
+ else if (qualityPreset === "medium") baseQuality = 50;
445
+ else if (qualityPreset === "high") baseQuality = 75;
446
+ else if (qualityPreset) {
447
+ const n = parseInt(qualityPreset, 10);
448
+ if (!isNaN(n)) baseQuality = Math.min(100, Math.max(1, n));
449
+ }
450
+ const displayFactor = display && DISPLAY_PRESETS[display] ? DISPLAY_PRESETS[display] : 1;
451
+ baseQuality = Math.round(baseQuality * displayFactor);
452
+ let quality = baseQuality;
453
+ let effort = 4;
454
+ let pngCompression = 6;
455
+ if (fileSizeInBytes) {
456
+ const sizeInKB = fileSizeInBytes / 1024;
457
+ if (sizeInKB > 500) {
458
+ quality = Math.min(baseQuality, 25);
459
+ effort = 9;
460
+ pngCompression = 9;
461
+ } else if (sizeInKB > 300) {
462
+ quality = Math.min(baseQuality, 30);
463
+ effort = 8;
464
+ pngCompression = 9;
465
+ } else if (sizeInKB > 150) {
466
+ quality = Math.min(baseQuality, 35);
467
+ effort = 7;
468
+ pngCompression = 8;
469
+ } else if (sizeInKB > 90) {
470
+ quality = Math.min(baseQuality, 40);
471
+ effort = 6;
472
+ pngCompression = 8;
473
+ } else if (sizeInKB > 50) {
474
+ quality = Math.min(baseQuality, 50);
475
+ effort = 5;
476
+ pngCompression = 7;
477
+ }
478
+ }
479
+ const dimensions = size && SIZE_PRESETS[size] ? SIZE_PRESETS[size] : void 0;
480
+ return {
481
+ quality: Math.max(1, Math.min(100, quality)),
482
+ effort,
483
+ pngCompression,
484
+ ...dimensions && { width: dimensions.width, height: dimensions.height }
485
+ };
379
486
  };
380
487
  var objectIdSchema = zod.z.string().refine((val) => mongoose.isValidObjectId(val), {
381
488
  message: "Invalid ObjectId format"
@@ -1561,24 +1668,32 @@ var driveAPIHandler = async (req, res) => {
1561
1668
  return;
1562
1669
  }
1563
1670
  if (action === "serve") {
1564
- const { stream, mime, size } = await itemProvider.openStream(drive, itemAccountId);
1671
+ const { stream, mime, size: fileSize } = await itemProvider.openStream(drive, itemAccountId);
1565
1672
  const safeFilename = sanitizeContentDispositionFilename(drive.name);
1566
1673
  const format = req.query.format;
1567
1674
  const quality = req.query.quality;
1675
+ const display = req.query.display;
1676
+ const sizePreset = req.query.size;
1568
1677
  const isImage = mime.startsWith("image/");
1569
- const shouldTransform = isImage && (format || quality);
1678
+ const shouldTransform = isImage && (format || quality || display || sizePreset);
1570
1679
  res.setHeader("Content-Disposition", `inline; filename="${safeFilename}"`);
1571
1680
  if (config.cors?.enabled) {
1572
1681
  res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
1573
1682
  }
1574
1683
  if (shouldTransform) {
1575
1684
  try {
1576
- const qValue = parseQuality(quality);
1685
+ const settings = getImageSettings(fileSize, quality, display, sizePreset);
1577
1686
  let targetFormat = format || mime.split("/")[1];
1578
1687
  if (targetFormat === "jpg") targetFormat = "jpeg";
1579
1688
  const cacheDir = path__default.default.join(config.storage.path, "file", drive._id.toString(), "cache");
1580
- const cacheFilename = `optimized_q${qValue}_${targetFormat}.bin`;
1581
- const cachePath = path__default.default.join(cacheDir, cacheFilename);
1689
+ const cacheKey = [
1690
+ "opt",
1691
+ `q${settings.quality}`,
1692
+ `e${settings.effort}`,
1693
+ settings.width ? `${settings.width}x${settings.height}` : "orig",
1694
+ targetFormat
1695
+ ].join("_");
1696
+ const cachePath = path__default.default.join(cacheDir, `${cacheKey}.bin`);
1582
1697
  if (fs__default.default.existsSync(cachePath)) {
1583
1698
  const cacheStat = fs__default.default.statSync(cachePath);
1584
1699
  res.setHeader("Content-Type", `image/${targetFormat}`);
@@ -1592,18 +1707,24 @@ var driveAPIHandler = async (req, res) => {
1592
1707
  return;
1593
1708
  }
1594
1709
  if (!fs__default.default.existsSync(cacheDir)) fs__default.default.mkdirSync(cacheDir, { recursive: true });
1595
- const pipeline = sharp__default.default();
1710
+ let pipeline = sharp__default.default();
1711
+ if (settings.width && settings.height) {
1712
+ pipeline = pipeline.resize(settings.width, settings.height, {
1713
+ fit: "inside",
1714
+ withoutEnlargement: true
1715
+ });
1716
+ }
1596
1717
  if (targetFormat === "jpeg") {
1597
- pipeline.jpeg({ quality: qValue, mozjpeg: true });
1718
+ pipeline = pipeline.jpeg({ quality: settings.quality, mozjpeg: true });
1598
1719
  res.setHeader("Content-Type", "image/jpeg");
1599
1720
  } else if (targetFormat === "png") {
1600
- pipeline.png({ quality: qValue });
1721
+ pipeline = pipeline.png({ compressionLevel: settings.pngCompression, adaptiveFiltering: true });
1601
1722
  res.setHeader("Content-Type", "image/png");
1602
1723
  } else if (targetFormat === "webp") {
1603
- pipeline.webp({ quality: qValue });
1724
+ pipeline = pipeline.webp({ quality: settings.quality, effort: settings.effort });
1604
1725
  res.setHeader("Content-Type", "image/webp");
1605
1726
  } else if (targetFormat === "avif") {
1606
- pipeline.avif({ quality: qValue });
1727
+ pipeline = pipeline.avif({ quality: settings.quality, effort: settings.effort });
1607
1728
  res.setHeader("Content-Type", "image/avif");
1608
1729
  }
1609
1730
  res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
@@ -1616,7 +1737,7 @@ var driveAPIHandler = async (req, res) => {
1616
1737
  }
1617
1738
  }
1618
1739
  res.setHeader("Content-Type", mime);
1619
- if (size) res.setHeader("Content-Length", size);
1740
+ if (fileSize) res.setHeader("Content-Length", fileSize);
1620
1741
  stream.pipe(res);
1621
1742
  return;
1622
1743
  }
@@ -2161,5 +2282,5 @@ exports.driveReadFile = driveReadFile;
2161
2282
  exports.driveUpload = driveUpload;
2162
2283
  exports.getDriveConfig = getDriveConfig;
2163
2284
  exports.getDriveInformation = getDriveInformation;
2164
- //# sourceMappingURL=chunk-KQGZXSKY.cjs.map
2165
- //# sourceMappingURL=chunk-KQGZXSKY.cjs.map
2285
+ //# sourceMappingURL=chunk-I3AR7V7Z.cjs.map
2286
+ //# sourceMappingURL=chunk-I3AR7V7Z.cjs.map