@muhgholy/next-drive 4.4.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 +116 -30
- package/dist/{chunk-ZNXH3LYN.cjs → chunk-I3AR7V7Z.cjs} +182 -5
- package/dist/chunk-I3AR7V7Z.cjs.map +1 -0
- package/dist/{chunk-5CAP2MNG.js → chunk-UT2XCOS7.js} +182 -5
- package/dist/chunk-UT2XCOS7.js.map +1 -0
- package/dist/client/index.cjs +1 -1
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/server/express.cjs +11 -11
- package/dist/server/express.js +2 -2
- package/dist/server/index.cjs +13 -13
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/utils.d.ts +21 -0
- package/dist/server/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-5CAP2MNG.js.map +0 -1
- package/dist/chunk-ZNXH3LYN.cjs.map +0 -1
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
|
-
##
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
|
599
|
-
|
|
600
|
-
| `
|
|
601
|
-
| `
|
|
602
|
-
| `
|
|
603
|
-
| `
|
|
604
|
-
|
|
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,6 +369,121 @@ var extractImageMetadata = async (filePath) => {
|
|
|
369
369
|
return null;
|
|
370
370
|
}
|
|
371
371
|
};
|
|
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
|
+
};
|
|
486
|
+
};
|
|
372
487
|
var objectIdSchema = zod.z.string().refine((val) => mongoose.isValidObjectId(val), {
|
|
373
488
|
message: "Invalid ObjectId format"
|
|
374
489
|
});
|
|
@@ -1553,14 +1668,76 @@ var driveAPIHandler = async (req, res) => {
|
|
|
1553
1668
|
return;
|
|
1554
1669
|
}
|
|
1555
1670
|
if (action === "serve") {
|
|
1556
|
-
const { stream, mime, size } = await itemProvider.openStream(drive, itemAccountId);
|
|
1671
|
+
const { stream, mime, size: fileSize } = await itemProvider.openStream(drive, itemAccountId);
|
|
1557
1672
|
const safeFilename = sanitizeContentDispositionFilename(drive.name);
|
|
1673
|
+
const format = req.query.format;
|
|
1674
|
+
const quality = req.query.quality;
|
|
1675
|
+
const display = req.query.display;
|
|
1676
|
+
const sizePreset = req.query.size;
|
|
1677
|
+
const isImage = mime.startsWith("image/");
|
|
1678
|
+
const shouldTransform = isImage && (format || quality || display || sizePreset);
|
|
1558
1679
|
res.setHeader("Content-Disposition", `inline; filename="${safeFilename}"`);
|
|
1559
|
-
res.setHeader("Content-Type", mime);
|
|
1560
1680
|
if (config.cors?.enabled) {
|
|
1561
1681
|
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
1562
1682
|
}
|
|
1563
|
-
if (
|
|
1683
|
+
if (shouldTransform) {
|
|
1684
|
+
try {
|
|
1685
|
+
const settings = getImageSettings(fileSize, quality, display, sizePreset);
|
|
1686
|
+
let targetFormat = format || mime.split("/")[1];
|
|
1687
|
+
if (targetFormat === "jpg") targetFormat = "jpeg";
|
|
1688
|
+
const cacheDir = path__default.default.join(config.storage.path, "file", drive._id.toString(), "cache");
|
|
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`);
|
|
1697
|
+
if (fs__default.default.existsSync(cachePath)) {
|
|
1698
|
+
const cacheStat = fs__default.default.statSync(cachePath);
|
|
1699
|
+
res.setHeader("Content-Type", `image/${targetFormat}`);
|
|
1700
|
+
res.setHeader("Content-Length", cacheStat.size);
|
|
1701
|
+
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
1702
|
+
if (config.cors?.enabled) {
|
|
1703
|
+
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
1704
|
+
}
|
|
1705
|
+
if ("destroy" in stream) stream.destroy();
|
|
1706
|
+
fs__default.default.createReadStream(cachePath).pipe(res);
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
if (!fs__default.default.existsSync(cacheDir)) fs__default.default.mkdirSync(cacheDir, { recursive: true });
|
|
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
|
+
}
|
|
1717
|
+
if (targetFormat === "jpeg") {
|
|
1718
|
+
pipeline = pipeline.jpeg({ quality: settings.quality, mozjpeg: true });
|
|
1719
|
+
res.setHeader("Content-Type", "image/jpeg");
|
|
1720
|
+
} else if (targetFormat === "png") {
|
|
1721
|
+
pipeline = pipeline.png({ compressionLevel: settings.pngCompression, adaptiveFiltering: true });
|
|
1722
|
+
res.setHeader("Content-Type", "image/png");
|
|
1723
|
+
} else if (targetFormat === "webp") {
|
|
1724
|
+
pipeline = pipeline.webp({ quality: settings.quality, effort: settings.effort });
|
|
1725
|
+
res.setHeader("Content-Type", "image/webp");
|
|
1726
|
+
} else if (targetFormat === "avif") {
|
|
1727
|
+
pipeline = pipeline.avif({ quality: settings.quality, effort: settings.effort });
|
|
1728
|
+
res.setHeader("Content-Type", "image/avif");
|
|
1729
|
+
}
|
|
1730
|
+
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
1731
|
+
stream.pipe(pipeline);
|
|
1732
|
+
pipeline.clone().toFile(cachePath).catch((e) => console.error("[next-drive] Cache write failed:", e));
|
|
1733
|
+
pipeline.clone().pipe(res);
|
|
1734
|
+
return;
|
|
1735
|
+
} catch (e) {
|
|
1736
|
+
console.error("[next-drive] Image transformation failed:", e);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
res.setHeader("Content-Type", mime);
|
|
1740
|
+
if (fileSize) res.setHeader("Content-Length", fileSize);
|
|
1564
1741
|
stream.pipe(res);
|
|
1565
1742
|
return;
|
|
1566
1743
|
}
|
|
@@ -2105,5 +2282,5 @@ exports.driveReadFile = driveReadFile;
|
|
|
2105
2282
|
exports.driveUpload = driveUpload;
|
|
2106
2283
|
exports.getDriveConfig = getDriveConfig;
|
|
2107
2284
|
exports.getDriveInformation = getDriveInformation;
|
|
2108
|
-
//# sourceMappingURL=chunk-
|
|
2109
|
-
//# sourceMappingURL=chunk-
|
|
2285
|
+
//# sourceMappingURL=chunk-I3AR7V7Z.cjs.map
|
|
2286
|
+
//# sourceMappingURL=chunk-I3AR7V7Z.cjs.map
|