@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 +116 -30
- package/dist/{chunk-KQGZXSKY.cjs → chunk-I3AR7V7Z.cjs} +141 -20
- package/dist/chunk-I3AR7V7Z.cjs.map +1 -0
- package/dist/{chunk-YUU5BFE7.js → chunk-UT2XCOS7.js} +141 -20
- package/dist/chunk-UT2XCOS7.js.map +1 -0
- 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 -1
- package/dist/server/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-KQGZXSKY.cjs.map +0 -1
- package/dist/chunk-YUU5BFE7.js.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,13 +369,120 @@ var extractImageMetadata = async (filePath) => {
|
|
|
369
369
|
return null;
|
|
370
370
|
}
|
|
371
371
|
};
|
|
372
|
-
var
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
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
|
|
1581
|
-
|
|
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
|
-
|
|
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:
|
|
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({
|
|
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:
|
|
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:
|
|
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 (
|
|
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-
|
|
2165
|
-
//# sourceMappingURL=chunk-
|
|
2285
|
+
//# sourceMappingURL=chunk-I3AR7V7Z.cjs.map
|
|
2286
|
+
//# sourceMappingURL=chunk-I3AR7V7Z.cjs.map
|