@gallop.software/studio 0.1.116 → 1.0.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.
@@ -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,13 +309,16 @@ 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.isDirectory() && !entry.name.startsWith(".")) {
317
318
  if (!seenFolders.has(entry.name)) {
318
319
  seenFolders.add(entry.name);
320
+ const isImagesFolder = entry.name === "images" && !relativePath;
321
+ const folderPath = relativePath ? `public/${relativePath}/${entry.name}` : `public/${entry.name}`;
319
322
  const folderPrefix = pathPrefix === "/" ? `/${entry.name}/` : `${pathPrefix}${entry.name}/`;
320
323
  let fileCount = 0;
321
324
  for (const k of metaKeys) {
@@ -323,9 +326,10 @@ async function handleList(request) {
323
326
  }
324
327
  items.push({
325
328
  name: entry.name,
326
- path: relativePath ? `public/${relativePath}/${entry.name}` : `public/${entry.name}`,
329
+ path: folderPath,
327
330
  type: "folder",
328
- fileCount
331
+ fileCount,
332
+ isProtected: isImagesFolder || isInsideImagesFolder
329
333
  });
330
334
  }
331
335
  }
@@ -354,7 +358,8 @@ async function handleList(request) {
354
358
  name: folderName,
355
359
  path: relativePath ? `public/${relativePath}/${folderName}` : `public/${folderName}`,
356
360
  type: "folder",
357
- fileCount
361
+ fileCount,
362
+ isProtected: isInsideImagesFolder
358
363
  });
359
364
  }
360
365
  } else {
@@ -413,6 +418,7 @@ async function handleList(request) {
413
418
  cdnPushed: isPushedToCloud,
414
419
  cdnBaseUrl: fileCdnUrl,
415
420
  isRemote,
421
+ isProtected: isInsideImagesFolder,
416
422
  dimensions: entry.w && entry.h ? { width: entry.w, height: entry.h } : void 0
417
423
  });
418
424
  }
@@ -1429,6 +1435,7 @@ async function handleProcessAllStream() {
1429
1435
  }
1430
1436
 
1431
1437
  // src/handlers/scan.ts
1438
+ import { NextResponse as NextResponse4 } from "next/server";
1432
1439
  import { promises as fs7 } from "fs";
1433
1440
  import path8 from "path";
1434
1441
  import sharp3 from "sharp";
@@ -1444,11 +1451,12 @@ async function handleScanStream() {
1444
1451
  };
1445
1452
  try {
1446
1453
  const meta = await loadMeta();
1447
- const existingCount = Object.keys(meta).length;
1454
+ const existingCount = Object.keys(meta).filter((k) => !k.startsWith("_")).length;
1448
1455
  const existingKeys = new Set(Object.keys(meta));
1449
1456
  const added = [];
1450
1457
  const renamed = [];
1451
1458
  const errors = [];
1459
+ const orphanedFiles = [];
1452
1460
  const allFiles = [];
1453
1461
  async function scanDir(dir, relativePath = "") {
1454
1462
  try {
@@ -1538,6 +1546,40 @@ async function handleScanStream() {
1538
1546
  errors.push(relativePath);
1539
1547
  }
1540
1548
  }
1549
+ sendEvent({ type: "cleanup", message: "Checking for orphaned thumbnails..." });
1550
+ const expectedThumbnails = /* @__PURE__ */ new Set();
1551
+ const fileEntries = getFileEntries(meta);
1552
+ for (const [imageKey, entry] of fileEntries) {
1553
+ if (entry.c === void 0 && entry.p === 1) {
1554
+ for (const thumbPath of getAllThumbnailPaths(imageKey)) {
1555
+ expectedThumbnails.add(thumbPath);
1556
+ }
1557
+ }
1558
+ }
1559
+ async function findOrphans(dir, relativePath = "") {
1560
+ try {
1561
+ const entries = await fs7.readdir(dir, { withFileTypes: true });
1562
+ for (const entry of entries) {
1563
+ if (entry.name.startsWith(".")) continue;
1564
+ const fullPath = path8.join(dir, entry.name);
1565
+ const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
1566
+ if (entry.isDirectory()) {
1567
+ await findOrphans(fullPath, relPath);
1568
+ } else if (isImageFile(entry.name)) {
1569
+ const publicPath = `/images/${relPath}`;
1570
+ if (!expectedThumbnails.has(publicPath)) {
1571
+ orphanedFiles.push(publicPath);
1572
+ }
1573
+ }
1574
+ }
1575
+ } catch {
1576
+ }
1577
+ }
1578
+ const imagesDir = path8.join(process.cwd(), "public", "images");
1579
+ try {
1580
+ await findOrphans(imagesDir);
1581
+ } catch {
1582
+ }
1541
1583
  await saveMeta(meta);
1542
1584
  sendEvent({
1543
1585
  type: "complete",
@@ -1545,7 +1587,8 @@ async function handleScanStream() {
1545
1587
  added: added.length,
1546
1588
  renamed: renamed.length,
1547
1589
  errors: errors.length,
1548
- renamedFiles: renamed
1590
+ renamedFiles: renamed,
1591
+ orphanedFiles: orphanedFiles.length > 0 ? orphanedFiles : void 0
1549
1592
  });
1550
1593
  } catch (error) {
1551
1594
  console.error("Scan failed:", error);
@@ -1563,6 +1606,63 @@ async function handleScanStream() {
1563
1606
  }
1564
1607
  });
1565
1608
  }
1609
+ async function handleDeleteOrphans(request) {
1610
+ try {
1611
+ const { paths } = await request.json();
1612
+ if (!paths || !Array.isArray(paths) || paths.length === 0) {
1613
+ return NextResponse4.json({ error: "No paths provided" }, { status: 400 });
1614
+ }
1615
+ const deleted = [];
1616
+ const errors = [];
1617
+ for (const orphanPath of paths) {
1618
+ if (!orphanPath.startsWith("/images/")) {
1619
+ errors.push(`Invalid path: ${orphanPath}`);
1620
+ continue;
1621
+ }
1622
+ const fullPath = path8.join(process.cwd(), "public", orphanPath);
1623
+ try {
1624
+ await fs7.unlink(fullPath);
1625
+ deleted.push(orphanPath);
1626
+ } catch (err) {
1627
+ console.error(`Failed to delete ${orphanPath}:`, err);
1628
+ errors.push(orphanPath);
1629
+ }
1630
+ }
1631
+ const imagesDir = path8.join(process.cwd(), "public", "images");
1632
+ async function removeEmptyDirs(dir) {
1633
+ try {
1634
+ const entries = await fs7.readdir(dir, { withFileTypes: true });
1635
+ let isEmpty = true;
1636
+ for (const entry of entries) {
1637
+ if (entry.isDirectory()) {
1638
+ const subDirEmpty = await removeEmptyDirs(path8.join(dir, entry.name));
1639
+ if (!subDirEmpty) isEmpty = false;
1640
+ } else {
1641
+ isEmpty = false;
1642
+ }
1643
+ }
1644
+ if (isEmpty && dir !== imagesDir) {
1645
+ await fs7.rmdir(dir);
1646
+ }
1647
+ return isEmpty;
1648
+ } catch {
1649
+ return true;
1650
+ }
1651
+ }
1652
+ try {
1653
+ await removeEmptyDirs(imagesDir);
1654
+ } catch {
1655
+ }
1656
+ return NextResponse4.json({
1657
+ success: true,
1658
+ deleted: deleted.length,
1659
+ errors: errors.length
1660
+ });
1661
+ } catch (error) {
1662
+ console.error("Failed to delete orphans:", error);
1663
+ return NextResponse4.json({ error: "Failed to delete orphaned files" }, { status: 500 });
1664
+ }
1665
+ }
1566
1666
 
1567
1667
  // src/handlers/import.ts
1568
1668
  import sharp4 from "sharp";
@@ -1693,7 +1793,7 @@ async function handleUpdateCdns(request) {
1693
1793
  // src/handlers/index.ts
1694
1794
  async function GET(request) {
1695
1795
  if (process.env.NODE_ENV !== "development") {
1696
- return NextResponse4.json({ error: "Not available in production" }, { status: 403 });
1796
+ return NextResponse5.json({ error: "Not available in production" }, { status: 403 });
1697
1797
  }
1698
1798
  const pathname = request.nextUrl.pathname;
1699
1799
  const route = pathname.replace(/^\/api\/studio\/?/, "");
@@ -1715,11 +1815,11 @@ async function GET(request) {
1715
1815
  if (route === "cdns") {
1716
1816
  return handleGetCdns();
1717
1817
  }
1718
- return NextResponse4.json({ error: "Not found" }, { status: 404 });
1818
+ return NextResponse5.json({ error: "Not found" }, { status: 404 });
1719
1819
  }
1720
1820
  async function POST(request) {
1721
1821
  if (process.env.NODE_ENV !== "development") {
1722
- return NextResponse4.json({ error: "Not available in production" }, { status: 403 });
1822
+ return NextResponse5.json({ error: "Not available in production" }, { status: 403 });
1723
1823
  }
1724
1824
  const pathname = request.nextUrl.pathname;
1725
1825
  const route = pathname.replace(/^\/api\/studio\/?/, "");
@@ -1750,17 +1850,20 @@ async function POST(request) {
1750
1850
  if (route === "scan") {
1751
1851
  return handleScanStream();
1752
1852
  }
1853
+ if (route === "delete-orphans") {
1854
+ return handleDeleteOrphans(request);
1855
+ }
1753
1856
  if (route === "import") {
1754
1857
  return handleImportUrls(request);
1755
1858
  }
1756
1859
  if (route === "cdns") {
1757
1860
  return handleUpdateCdns(request);
1758
1861
  }
1759
- return NextResponse4.json({ error: "Not found" }, { status: 404 });
1862
+ return NextResponse5.json({ error: "Not found" }, { status: 404 });
1760
1863
  }
1761
1864
  async function DELETE(request) {
1762
1865
  if (process.env.NODE_ENV !== "development") {
1763
- return NextResponse4.json({ error: "Not available in production" }, { status: 403 });
1866
+ return NextResponse5.json({ error: "Not available in production" }, { status: 403 });
1764
1867
  }
1765
1868
  return handleDelete(request);
1766
1869
  }