@gallop.software/studio 0.1.24 → 0.1.26

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/dist/handlers.mjs CHANGED
@@ -22,8 +22,8 @@ async function GET(request) {
22
22
  if (route === "scan") {
23
23
  return handleScan();
24
24
  }
25
- if (route === "count-unprocessed") {
26
- return handleCountUnprocessed();
25
+ if (route === "count-images") {
26
+ return handleCountImages();
27
27
  }
28
28
  return NextResponse.json({ error: "Not found" }, { status: 404 });
29
29
  }
@@ -46,7 +46,7 @@ async function POST(request) {
46
46
  return handleReprocess(request);
47
47
  }
48
48
  if (route === "process-all") {
49
- return handleProcessAll();
49
+ return handleProcessAllStream();
50
50
  }
51
51
  return NextResponse.json({ error: "Not found" }, { status: 404 });
52
52
  }
@@ -494,10 +494,9 @@ async function handleReprocess(request) {
494
494
  return NextResponse.json({ error: "Failed to reprocess images" }, { status: 500 });
495
495
  }
496
496
  }
497
- async function handleCountUnprocessed() {
497
+ async function handleCountImages() {
498
498
  try {
499
- const meta = await loadMeta();
500
- const unprocessedImages = [];
499
+ const allImages = [];
501
500
  async function scanPublicFolder(dir, relativePath = "") {
502
501
  try {
503
502
  const entries = await fs.readdir(dir, { withFileTypes: true });
@@ -509,9 +508,7 @@ async function handleCountUnprocessed() {
509
508
  if (entry.isDirectory()) {
510
509
  await scanPublicFolder(fullPath, relPath);
511
510
  } else if (isImageFile(entry.name)) {
512
- if (!meta.images[relPath]) {
513
- unprocessedImages.push(relPath);
514
- }
511
+ allImages.push(relPath);
515
512
  }
516
513
  }
517
514
  } catch {
@@ -520,163 +517,192 @@ async function handleCountUnprocessed() {
520
517
  const publicDir = path.join(process.cwd(), "public");
521
518
  await scanPublicFolder(publicDir);
522
519
  return NextResponse.json({
523
- count: unprocessedImages.length,
524
- images: unprocessedImages
520
+ count: allImages.length,
521
+ images: allImages
525
522
  });
526
523
  } catch (error) {
527
- console.error("Failed to count unprocessed images:", error);
528
- return NextResponse.json({ error: "Failed to count unprocessed images" }, { status: 500 });
524
+ console.error("Failed to count images:", error);
525
+ return NextResponse.json({ error: "Failed to count images" }, { status: 500 });
529
526
  }
530
527
  }
531
- async function handleProcessAll() {
532
- try {
533
- const meta = await loadMeta();
534
- const processed = [];
535
- const errors = [];
536
- const orphansRemoved = [];
537
- const unprocessedImages = [];
538
- async function scanPublicFolder(dir, relativePath = "") {
528
+ async function handleProcessAllStream() {
529
+ const encoder = new TextEncoder();
530
+ const stream = new ReadableStream({
531
+ async start(controller) {
532
+ const sendEvent = (data) => {
533
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
534
+
535
+ `));
536
+ };
539
537
  try {
540
- const entries = await fs.readdir(dir, { withFileTypes: true });
541
- for (const entry of entries) {
542
- if (entry.name.startsWith(".")) continue;
543
- const fullPath = path.join(dir, entry.name);
544
- const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
545
- if (relPath === "images" || relPath.startsWith("images/")) continue;
546
- if (entry.isDirectory()) {
547
- await scanPublicFolder(fullPath, relPath);
548
- } else if (isImageFile(entry.name)) {
549
- if (!meta.images[relPath]) {
550
- unprocessedImages.push({ key: relPath, fullPath });
538
+ const meta = await loadMeta();
539
+ const processed = [];
540
+ const errors = [];
541
+ const orphansRemoved = [];
542
+ const allImages = [];
543
+ async function scanPublicFolder(dir, relativePath = "") {
544
+ try {
545
+ const entries = await fs.readdir(dir, { withFileTypes: true });
546
+ for (const entry of entries) {
547
+ if (entry.name.startsWith(".")) continue;
548
+ const fullPath = path.join(dir, entry.name);
549
+ const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
550
+ if (relPath === "images" || relPath.startsWith("images/")) continue;
551
+ if (entry.isDirectory()) {
552
+ await scanPublicFolder(fullPath, relPath);
553
+ } else if (isImageFile(entry.name)) {
554
+ allImages.push({ key: relPath, fullPath });
555
+ }
551
556
  }
557
+ } catch {
552
558
  }
553
559
  }
554
- } catch {
555
- }
556
- }
557
- const publicDir = path.join(process.cwd(), "public");
558
- await scanPublicFolder(publicDir);
559
- for (const { key, fullPath } of unprocessedImages) {
560
- try {
561
- const buffer = await fs.readFile(fullPath);
562
- const ext = path.extname(key).toLowerCase();
563
- const isSvg = ext === ".svg";
564
- if (isSvg) {
565
- const imageDir = path.dirname(key);
566
- const imagesPath = path.join(process.cwd(), "public", "images", imageDir === "." ? "" : imageDir);
567
- await fs.mkdir(imagesPath, { recursive: true });
568
- const fileName = path.basename(key);
569
- const destPath = path.join(imagesPath, fileName);
570
- await fs.writeFile(destPath, buffer);
571
- const sizePath = `/images/${imageDir === "." ? "" : imageDir + "/"}${fileName}`;
572
- meta.images[key] = {
573
- original: {
574
- path: `/${key}`,
575
- width: 0,
576
- height: 0,
577
- fileSize: buffer.length
578
- },
579
- sizes: {
580
- full: { path: sizePath, width: 0, height: 0 },
581
- large: { path: sizePath, width: 0, height: 0 },
582
- medium: { path: sizePath, width: 0, height: 0 },
583
- small: { path: sizePath, width: 0, height: 0 }
584
- },
585
- blurhash: "",
586
- dominantColor: "#888888",
587
- cdn: null
588
- };
589
- } else {
590
- const dummyEntry = {
591
- original: {
592
- path: `/${key}`,
593
- width: 0,
594
- height: 0,
595
- fileSize: buffer.length
596
- },
597
- sizes: {
598
- full: { path: "", width: 0, height: 0 },
599
- large: { path: "", width: 0, height: 0 },
600
- medium: { path: "", width: 0, height: 0 },
601
- small: { path: "", width: 0, height: 0 }
602
- },
603
- blurhash: "",
604
- dominantColor: "#888888",
605
- cdn: null
606
- };
607
- const processedEntry = await processImage(buffer, dummyEntry, key);
608
- meta.images[key] = processedEntry;
560
+ const publicDir = path.join(process.cwd(), "public");
561
+ await scanPublicFolder(publicDir);
562
+ const total = allImages.length;
563
+ sendEvent({ type: "start", total });
564
+ for (let i = 0; i < allImages.length; i++) {
565
+ const { key, fullPath } = allImages[i];
566
+ sendEvent({
567
+ type: "progress",
568
+ current: i + 1,
569
+ total,
570
+ percent: Math.round((i + 1) / total * 100),
571
+ currentFile: key
572
+ });
573
+ try {
574
+ const buffer = await fs.readFile(fullPath);
575
+ const ext = path.extname(key).toLowerCase();
576
+ const isSvg = ext === ".svg";
577
+ if (isSvg) {
578
+ const imageDir = path.dirname(key);
579
+ const imagesPath = path.join(process.cwd(), "public", "images", imageDir === "." ? "" : imageDir);
580
+ await fs.mkdir(imagesPath, { recursive: true });
581
+ const fileName = path.basename(key);
582
+ const destPath = path.join(imagesPath, fileName);
583
+ await fs.writeFile(destPath, buffer);
584
+ const sizePath = `/images/${imageDir === "." ? "" : imageDir + "/"}${fileName}`;
585
+ meta.images[key] = {
586
+ original: {
587
+ path: `/${key}`,
588
+ width: 0,
589
+ height: 0,
590
+ fileSize: buffer.length
591
+ },
592
+ sizes: {
593
+ full: { path: sizePath, width: 0, height: 0 },
594
+ large: { path: sizePath, width: 0, height: 0 },
595
+ medium: { path: sizePath, width: 0, height: 0 },
596
+ small: { path: sizePath, width: 0, height: 0 }
597
+ },
598
+ blurhash: "",
599
+ dominantColor: "#888888",
600
+ cdn: null
601
+ };
602
+ } else {
603
+ const existingEntry = meta.images[key];
604
+ const baseEntry = existingEntry || {
605
+ original: {
606
+ path: `/${key}`,
607
+ width: 0,
608
+ height: 0,
609
+ fileSize: buffer.length
610
+ },
611
+ sizes: {
612
+ full: { path: "", width: 0, height: 0 },
613
+ large: { path: "", width: 0, height: 0 },
614
+ medium: { path: "", width: 0, height: 0 },
615
+ small: { path: "", width: 0, height: 0 }
616
+ },
617
+ blurhash: "",
618
+ dominantColor: "#888888",
619
+ cdn: null
620
+ };
621
+ const processedEntry = await processImage(buffer, baseEntry, key);
622
+ meta.images[key] = processedEntry;
623
+ }
624
+ processed.push(key);
625
+ } catch (error) {
626
+ console.error(`Failed to process ${key}:`, error);
627
+ errors.push(key);
628
+ }
609
629
  }
610
- processed.push(key);
611
- } catch (error) {
612
- console.error(`Failed to process ${key}:`, error);
613
- errors.push(key);
614
- }
615
- }
616
- const trackedPaths = /* @__PURE__ */ new Set();
617
- for (const entry of Object.values(meta.images)) {
618
- for (const sizeData of Object.values(entry.sizes)) {
619
- trackedPaths.add(sizeData.path);
620
- }
621
- }
622
- async function findOrphans(dir, relativePath = "") {
623
- try {
624
- const entries = await fs.readdir(dir, { withFileTypes: true });
625
- for (const entry of entries) {
626
- if (entry.name.startsWith(".")) continue;
627
- const fullPath = path.join(dir, entry.name);
628
- const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
629
- if (entry.isDirectory()) {
630
- await findOrphans(fullPath, relPath);
631
- } else if (isImageFile(entry.name)) {
632
- const publicPath = `/images/${relPath}`;
633
- if (!trackedPaths.has(publicPath)) {
634
- try {
635
- await fs.unlink(fullPath);
636
- orphansRemoved.push(publicPath);
637
- } catch (err) {
638
- console.error(`Failed to remove orphan ${publicPath}:`, err);
630
+ sendEvent({ type: "cleanup", message: "Removing orphaned thumbnails..." });
631
+ const trackedPaths = /* @__PURE__ */ new Set();
632
+ for (const entry of Object.values(meta.images)) {
633
+ for (const sizeData of Object.values(entry.sizes)) {
634
+ trackedPaths.add(sizeData.path);
635
+ }
636
+ }
637
+ async function findOrphans(dir, relativePath = "") {
638
+ try {
639
+ const entries = await fs.readdir(dir, { withFileTypes: true });
640
+ for (const entry of entries) {
641
+ if (entry.name.startsWith(".")) continue;
642
+ const fullPath = path.join(dir, entry.name);
643
+ const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
644
+ if (entry.isDirectory()) {
645
+ await findOrphans(fullPath, relPath);
646
+ } else if (isImageFile(entry.name)) {
647
+ const publicPath = `/images/${relPath}`;
648
+ if (!trackedPaths.has(publicPath)) {
649
+ try {
650
+ await fs.unlink(fullPath);
651
+ orphansRemoved.push(publicPath);
652
+ } catch (err) {
653
+ console.error(`Failed to remove orphan ${publicPath}:`, err);
654
+ }
655
+ }
639
656
  }
640
657
  }
658
+ } catch {
641
659
  }
642
660
  }
643
- } catch {
644
- }
645
- }
646
- const imagesDir = path.join(process.cwd(), "public", "images");
647
- await findOrphans(imagesDir);
648
- async function removeEmptyDirs(dir) {
649
- try {
650
- const entries = await fs.readdir(dir, { withFileTypes: true });
651
- let isEmpty = true;
652
- for (const entry of entries) {
653
- if (entry.isDirectory()) {
654
- const subDirEmpty = await removeEmptyDirs(path.join(dir, entry.name));
655
- if (!subDirEmpty) isEmpty = false;
656
- } else {
657
- isEmpty = false;
661
+ const imagesDir = path.join(process.cwd(), "public", "images");
662
+ await findOrphans(imagesDir);
663
+ async function removeEmptyDirs(dir) {
664
+ try {
665
+ const entries = await fs.readdir(dir, { withFileTypes: true });
666
+ let isEmpty = true;
667
+ for (const entry of entries) {
668
+ if (entry.isDirectory()) {
669
+ const subDirEmpty = await removeEmptyDirs(path.join(dir, entry.name));
670
+ if (!subDirEmpty) isEmpty = false;
671
+ } else {
672
+ isEmpty = false;
673
+ }
674
+ }
675
+ if (isEmpty && dir !== imagesDir) {
676
+ await fs.rmdir(dir);
677
+ }
678
+ return isEmpty;
679
+ } catch {
680
+ return true;
658
681
  }
659
682
  }
660
- if (isEmpty && dir !== imagesDir) {
661
- await fs.rmdir(dir);
662
- }
663
- return isEmpty;
664
- } catch {
665
- return true;
683
+ await removeEmptyDirs(imagesDir);
684
+ await saveMeta(meta);
685
+ sendEvent({
686
+ type: "complete",
687
+ processed: processed.length,
688
+ orphansRemoved: orphansRemoved.length,
689
+ errors: errors.length
690
+ });
691
+ } catch (error) {
692
+ console.error("Failed to process all:", error);
693
+ sendEvent({ type: "error", message: "Failed to process images" });
694
+ } finally {
695
+ controller.close();
666
696
  }
667
697
  }
668
- await removeEmptyDirs(imagesDir);
669
- await saveMeta(meta);
670
- return NextResponse.json({
671
- success: true,
672
- processed,
673
- orphansRemoved,
674
- errors: errors.length > 0 ? errors : void 0
675
- });
676
- } catch (error) {
677
- console.error("Failed to process all:", error);
678
- return NextResponse.json({ error: "Failed to process all images" }, { status: 500 });
679
- }
698
+ });
699
+ return new Response(stream, {
700
+ headers: {
701
+ "Content-Type": "text/event-stream",
702
+ "Cache-Control": "no-cache",
703
+ "Connection": "keep-alive"
704
+ }
705
+ });
680
706
  }
681
707
  async function loadMeta() {
682
708
  const metaPath = path.join(process.cwd(), "_data", "_meta.json");