@gallop.software/studio 0.1.116 → 1.0.1

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.
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  getAllThumbnailPaths,
3
3
  getThumbnailPath
4
- } from "../chunk-RDNC5ABF.mjs";
4
+ } from "../chunk-FDWPNRNZ.mjs";
5
5
 
6
6
  // src/handlers/index.ts
7
- import { NextResponse as NextResponse4 } from "next/server";
7
+ import { NextResponse as NextResponse5 } from "next/server";
8
8
 
9
9
  // src/handlers/list.ts
10
10
  import { NextResponse } from "next/server";
@@ -309,25 +309,58 @@ async function handleList(request) {
309
309
  const items = [];
310
310
  const seenFolders = /* @__PURE__ */ new Set();
311
311
  const metaKeys = fileEntries.map(([key]) => key);
312
+ const isInsideImagesFolder = relativePath === "images" || relativePath.startsWith("images/");
312
313
  const absoluteDir = path5.join(process.cwd(), requestedPath);
313
314
  try {
314
315
  const dirEntries = await fs4.readdir(absoluteDir, { withFileTypes: true });
315
316
  for (const entry of dirEntries) {
316
- if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "images") {
317
+ if (entry.name.startsWith(".")) continue;
318
+ if (entry.isDirectory()) {
317
319
  if (!seenFolders.has(entry.name)) {
318
320
  seenFolders.add(entry.name);
319
- const folderPrefix = pathPrefix === "/" ? `/${entry.name}/` : `${pathPrefix}${entry.name}/`;
321
+ const isImagesFolder = entry.name === "images" && !relativePath;
322
+ const folderPath = relativePath ? `public/${relativePath}/${entry.name}` : `public/${entry.name}`;
320
323
  let fileCount = 0;
321
- for (const k of metaKeys) {
322
- if (k.startsWith(folderPrefix)) fileCount++;
324
+ if (isInsideImagesFolder || isImagesFolder) {
325
+ const subDir = path5.join(absoluteDir, entry.name);
326
+ try {
327
+ const subEntries = await fs4.readdir(subDir);
328
+ fileCount = subEntries.filter((f) => !f.startsWith(".")).length;
329
+ } catch {
330
+ }
331
+ } else {
332
+ const folderPrefix = pathPrefix === "/" ? `/${entry.name}/` : `${pathPrefix}${entry.name}/`;
333
+ for (const k of metaKeys) {
334
+ if (k.startsWith(folderPrefix)) fileCount++;
335
+ }
323
336
  }
324
337
  items.push({
325
338
  name: entry.name,
326
- path: relativePath ? `public/${relativePath}/${entry.name}` : `public/${entry.name}`,
339
+ path: folderPath,
327
340
  type: "folder",
328
- fileCount
341
+ fileCount,
342
+ isProtected: isImagesFolder || isInsideImagesFolder
329
343
  });
330
344
  }
345
+ } else if (isInsideImagesFolder) {
346
+ const filePath = relativePath ? `public/${relativePath}/${entry.name}` : `public/${entry.name}`;
347
+ const fullPath = path5.join(absoluteDir, entry.name);
348
+ let fileSize;
349
+ try {
350
+ const stats = await fs4.stat(fullPath);
351
+ fileSize = stats.size;
352
+ } catch {
353
+ }
354
+ const isImage = isImageFile(entry.name);
355
+ items.push({
356
+ name: entry.name,
357
+ path: filePath,
358
+ type: "file",
359
+ size: fileSize,
360
+ thumbnail: isImage ? `/${relativePath}/${entry.name}` : void 0,
361
+ hasThumbnail: false,
362
+ isProtected: true
363
+ });
331
364
  }
332
365
  }
333
366
  } catch {
@@ -354,7 +387,8 @@ async function handleList(request) {
354
387
  name: folderName,
355
388
  path: relativePath ? `public/${relativePath}/${folderName}` : `public/${folderName}`,
356
389
  type: "folder",
357
- fileCount
390
+ fileCount,
391
+ isProtected: isInsideImagesFolder
358
392
  });
359
393
  }
360
394
  } else {
@@ -413,6 +447,7 @@ async function handleList(request) {
413
447
  cdnPushed: isPushedToCloud,
414
448
  cdnBaseUrl: fileCdnUrl,
415
449
  isRemote,
450
+ isProtected: isInsideImagesFolder,
416
451
  dimensions: entry.w && entry.h ? { width: entry.w, height: entry.h } : void 0
417
452
  });
418
453
  }
@@ -1429,6 +1464,7 @@ async function handleProcessAllStream() {
1429
1464
  }
1430
1465
 
1431
1466
  // src/handlers/scan.ts
1467
+ import { NextResponse as NextResponse4 } from "next/server";
1432
1468
  import { promises as fs7 } from "fs";
1433
1469
  import path8 from "path";
1434
1470
  import sharp3 from "sharp";
@@ -1444,11 +1480,12 @@ async function handleScanStream() {
1444
1480
  };
1445
1481
  try {
1446
1482
  const meta = await loadMeta();
1447
- const existingCount = Object.keys(meta).length;
1483
+ const existingCount = Object.keys(meta).filter((k) => !k.startsWith("_")).length;
1448
1484
  const existingKeys = new Set(Object.keys(meta));
1449
1485
  const added = [];
1450
1486
  const renamed = [];
1451
1487
  const errors = [];
1488
+ const orphanedFiles = [];
1452
1489
  const allFiles = [];
1453
1490
  async function scanDir(dir, relativePath = "") {
1454
1491
  try {
@@ -1538,6 +1575,40 @@ async function handleScanStream() {
1538
1575
  errors.push(relativePath);
1539
1576
  }
1540
1577
  }
1578
+ sendEvent({ type: "cleanup", message: "Checking for orphaned thumbnails..." });
1579
+ const expectedThumbnails = /* @__PURE__ */ new Set();
1580
+ const fileEntries = getFileEntries(meta);
1581
+ for (const [imageKey, entry] of fileEntries) {
1582
+ if (entry.c === void 0 && entry.p === 1) {
1583
+ for (const thumbPath of getAllThumbnailPaths(imageKey)) {
1584
+ expectedThumbnails.add(thumbPath);
1585
+ }
1586
+ }
1587
+ }
1588
+ async function findOrphans(dir, relativePath = "") {
1589
+ try {
1590
+ const entries = await fs7.readdir(dir, { withFileTypes: true });
1591
+ for (const entry of entries) {
1592
+ if (entry.name.startsWith(".")) continue;
1593
+ const fullPath = path8.join(dir, entry.name);
1594
+ const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
1595
+ if (entry.isDirectory()) {
1596
+ await findOrphans(fullPath, relPath);
1597
+ } else if (isImageFile(entry.name)) {
1598
+ const publicPath = `/images/${relPath}`;
1599
+ if (!expectedThumbnails.has(publicPath)) {
1600
+ orphanedFiles.push(publicPath);
1601
+ }
1602
+ }
1603
+ }
1604
+ } catch {
1605
+ }
1606
+ }
1607
+ const imagesDir = path8.join(process.cwd(), "public", "images");
1608
+ try {
1609
+ await findOrphans(imagesDir);
1610
+ } catch {
1611
+ }
1541
1612
  await saveMeta(meta);
1542
1613
  sendEvent({
1543
1614
  type: "complete",
@@ -1545,7 +1616,8 @@ async function handleScanStream() {
1545
1616
  added: added.length,
1546
1617
  renamed: renamed.length,
1547
1618
  errors: errors.length,
1548
- renamedFiles: renamed
1619
+ renamedFiles: renamed,
1620
+ orphanedFiles: orphanedFiles.length > 0 ? orphanedFiles : void 0
1549
1621
  });
1550
1622
  } catch (error) {
1551
1623
  console.error("Scan failed:", error);
@@ -1563,6 +1635,63 @@ async function handleScanStream() {
1563
1635
  }
1564
1636
  });
1565
1637
  }
1638
+ async function handleDeleteOrphans(request) {
1639
+ try {
1640
+ const { paths } = await request.json();
1641
+ if (!paths || !Array.isArray(paths) || paths.length === 0) {
1642
+ return NextResponse4.json({ error: "No paths provided" }, { status: 400 });
1643
+ }
1644
+ const deleted = [];
1645
+ const errors = [];
1646
+ for (const orphanPath of paths) {
1647
+ if (!orphanPath.startsWith("/images/")) {
1648
+ errors.push(`Invalid path: ${orphanPath}`);
1649
+ continue;
1650
+ }
1651
+ const fullPath = path8.join(process.cwd(), "public", orphanPath);
1652
+ try {
1653
+ await fs7.unlink(fullPath);
1654
+ deleted.push(orphanPath);
1655
+ } catch (err) {
1656
+ console.error(`Failed to delete ${orphanPath}:`, err);
1657
+ errors.push(orphanPath);
1658
+ }
1659
+ }
1660
+ const imagesDir = path8.join(process.cwd(), "public", "images");
1661
+ async function removeEmptyDirs(dir) {
1662
+ try {
1663
+ const entries = await fs7.readdir(dir, { withFileTypes: true });
1664
+ let isEmpty = true;
1665
+ for (const entry of entries) {
1666
+ if (entry.isDirectory()) {
1667
+ const subDirEmpty = await removeEmptyDirs(path8.join(dir, entry.name));
1668
+ if (!subDirEmpty) isEmpty = false;
1669
+ } else {
1670
+ isEmpty = false;
1671
+ }
1672
+ }
1673
+ if (isEmpty && dir !== imagesDir) {
1674
+ await fs7.rmdir(dir);
1675
+ }
1676
+ return isEmpty;
1677
+ } catch {
1678
+ return true;
1679
+ }
1680
+ }
1681
+ try {
1682
+ await removeEmptyDirs(imagesDir);
1683
+ } catch {
1684
+ }
1685
+ return NextResponse4.json({
1686
+ success: true,
1687
+ deleted: deleted.length,
1688
+ errors: errors.length
1689
+ });
1690
+ } catch (error) {
1691
+ console.error("Failed to delete orphans:", error);
1692
+ return NextResponse4.json({ error: "Failed to delete orphaned files" }, { status: 500 });
1693
+ }
1694
+ }
1566
1695
 
1567
1696
  // src/handlers/import.ts
1568
1697
  import sharp4 from "sharp";
@@ -1693,7 +1822,7 @@ async function handleUpdateCdns(request) {
1693
1822
  // src/handlers/index.ts
1694
1823
  async function GET(request) {
1695
1824
  if (process.env.NODE_ENV !== "development") {
1696
- return NextResponse4.json({ error: "Not available in production" }, { status: 403 });
1825
+ return NextResponse5.json({ error: "Not available in production" }, { status: 403 });
1697
1826
  }
1698
1827
  const pathname = request.nextUrl.pathname;
1699
1828
  const route = pathname.replace(/^\/api\/studio\/?/, "");
@@ -1715,11 +1844,11 @@ async function GET(request) {
1715
1844
  if (route === "cdns") {
1716
1845
  return handleGetCdns();
1717
1846
  }
1718
- return NextResponse4.json({ error: "Not found" }, { status: 404 });
1847
+ return NextResponse5.json({ error: "Not found" }, { status: 404 });
1719
1848
  }
1720
1849
  async function POST(request) {
1721
1850
  if (process.env.NODE_ENV !== "development") {
1722
- return NextResponse4.json({ error: "Not available in production" }, { status: 403 });
1851
+ return NextResponse5.json({ error: "Not available in production" }, { status: 403 });
1723
1852
  }
1724
1853
  const pathname = request.nextUrl.pathname;
1725
1854
  const route = pathname.replace(/^\/api\/studio\/?/, "");
@@ -1750,17 +1879,20 @@ async function POST(request) {
1750
1879
  if (route === "scan") {
1751
1880
  return handleScanStream();
1752
1881
  }
1882
+ if (route === "delete-orphans") {
1883
+ return handleDeleteOrphans(request);
1884
+ }
1753
1885
  if (route === "import") {
1754
1886
  return handleImportUrls(request);
1755
1887
  }
1756
1888
  if (route === "cdns") {
1757
1889
  return handleUpdateCdns(request);
1758
1890
  }
1759
- return NextResponse4.json({ error: "Not found" }, { status: 404 });
1891
+ return NextResponse5.json({ error: "Not found" }, { status: 404 });
1760
1892
  }
1761
1893
  async function DELETE(request) {
1762
1894
  if (process.env.NODE_ENV !== "development") {
1763
- return NextResponse4.json({ error: "Not available in production" }, { status: 403 });
1895
+ return NextResponse5.json({ error: "Not available in production" }, { status: 403 });
1764
1896
  }
1765
1897
  return handleDelete(request);
1766
1898
  }