From 479c284dfc9040f24356a1fa4d47565076d401fa Mon Sep 17 00:00:00 2001 From: Ein Anderssono Date: Thu, 11 Jun 2026 21:44:55 +0200 Subject: [PATCH] v0.2.4: stop export loop on Ctrl+C instead of flooding failures - Add IsCancelled() to Bridge interface - Check bridge.IsCancelled() before each export in serial/parallel/backupTree - Parallel workers mark remaining slots as 'cancelled' instead of exporting - Add photos_request_is_cancelled to ObjC and C stub --- Makefile | 2 +- bridge/photokit_bridge.h | 2 ++ bridge/photokit_bridge.m | 4 ++++ bridge/photokit_bridge_stub.c | 4 ++++ cmd/photoscli/main.go | 11 +++++++++++ cmd/photoscli/main_test.go | 1 + internal/photos/bridge.go | 1 + internal/photos/cgo_bridge.go | 4 ++++ internal/photos/cgo_bridge_test_impl.go | 4 ++++ 9 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0069f45..041138b 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ BINARY := ./bin/photoscli MODULE := gitea.k3s.k0.nu/tools/photocli -VERSION := 0.2.3 +VERSION := 0.2.4 BRIDGE_DIR := bridge LDFLAGS := -X main.version=$(VERSION) OBJ := $(BRIDGE_DIR)/photokit_bridge.o diff --git a/bridge/photokit_bridge.h b/bridge/photokit_bridge.h index 618bff3..a8b18bd 100644 --- a/bridge/photokit_bridge.h +++ b/bridge/photokit_bridge.h @@ -28,6 +28,8 @@ char *photos_list_tree_json(void); void photos_request_cancel(void); +int photos_request_is_cancelled(void); + void photos_free_string(char *value); #ifdef __cplusplus diff --git a/bridge/photokit_bridge.m b/bridge/photokit_bridge.m index 3ed7b76..85f6e79 100644 --- a/bridge/photokit_bridge.m +++ b/bridge/photokit_bridge.m @@ -415,3 +415,7 @@ void photos_free_string(char *value) { void photos_request_cancel(void) { photos_cancelled = 1; } + +int photos_request_is_cancelled(void) { + return photos_cancelled; +} diff --git a/bridge/photokit_bridge_stub.c b/bridge/photokit_bridge_stub.c index 3620f5e..bd90354 100644 --- a/bridge/photokit_bridge_stub.c +++ b/bridge/photokit_bridge_stub.c @@ -74,6 +74,10 @@ void photos_request_cancel(void) { stub_cancelled = 1; } +int photos_request_is_cancelled(void) { + return stub_cancelled; +} + void photos_test_set_export_preview_json(const char *json) { stub_export_preview_json = json; } diff --git a/cmd/photoscli/main.go b/cmd/photoscli/main.go index fcb5294..a4ed8c9 100644 --- a/cmd/photoscli/main.go +++ b/cmd/photoscli/main.go @@ -263,6 +263,9 @@ func backupTree(nodes []photos.CollectionNode, outDir string, targetSize int, or total := 0 failed := 0 for _, node := range nodes { + if bridge.IsCancelled() { + break + } path := outDir + "/" + sanitizePathComponent(node.Name) if node.Kind == "folder" { n, t, f, err := backupTree(node.Children, path, targetSize, originals, stderr, bridge) @@ -306,6 +309,9 @@ func exportAssetsSerial(assets []photos.Asset, outDir string, targetSize int, or exported := 0 failed := 0 for i, a := range assets { + if bridge.IsCancelled() { + break + } result, exportErr := exportOne(bridge, a, outDir, targetSize, originals, i) progressBar(stderr, exported+failed+1, total, dirPrefix+result.Filename, result.Size, result.Cloud) if exportErr != nil { @@ -337,6 +343,11 @@ func exportAssetsParallel(assets []photos.Asset, outDir string, targetSize int, go func() { defer wg.Done() for i := range jobs { + if bridge.IsCancelled() { + slots[i].err = fmt.Errorf("cancelled") + close(slots[i].done) + continue + } result, exportErr := exportOne(bridge, assets[i], outDir, targetSize, originals, i) slots[i].result = result slots[i].err = exportErr diff --git a/cmd/photoscli/main_test.go b/cmd/photoscli/main_test.go index 36937e2..1cfea97 100644 --- a/cmd/photoscli/main_test.go +++ b/cmd/photoscli/main_test.go @@ -50,6 +50,7 @@ func (m *mockBridge) ExportOriginal(assetID, out string, index int) (photos.Expo return photos.ExportResult{Filename: "test.jpg", Size: 2048, Cloud: "cloud"}, nil } func (m *mockBridge) Cancel() { m.cancelled = true } +func (m *mockBridge) IsCancelled() bool { return m.cancelled } func runWith(args []string, b photos.Bridge) (string, string, int) { var out, err bytes.Buffer diff --git a/internal/photos/bridge.go b/internal/photos/bridge.go index b0bd809..d4dfd65 100644 --- a/internal/photos/bridge.go +++ b/internal/photos/bridge.go @@ -13,6 +13,7 @@ type Bridge interface { ExportPreview(assetID, outputDir string, targetSize, index int) (ExportResult, error) ExportOriginal(assetID, outputDir string, index int) (ExportResult, error) Cancel() + IsCancelled() bool } func ParseAlbumsJSON(jsonStr string) ([]Album, error) { diff --git a/internal/photos/cgo_bridge.go b/internal/photos/cgo_bridge.go index 0cb486a..312db4b 100644 --- a/internal/photos/cgo_bridge.go +++ b/internal/photos/cgo_bridge.go @@ -58,6 +58,10 @@ func (*CgoBridge) Cancel() { C.photos_request_cancel() } +func (*CgoBridge) IsCancelled() bool { + return C.photos_request_is_cancelled() != 0 +} + func (*CgoBridge) ExportPreview(assetID, outputDir string, targetSize, index int) (ExportResult, error) { cid := C.CString(assetID) defer C.free(unsafe.Pointer(cid)) diff --git a/internal/photos/cgo_bridge_test_impl.go b/internal/photos/cgo_bridge_test_impl.go index 72ff4d3..c65888c 100644 --- a/internal/photos/cgo_bridge_test_impl.go +++ b/internal/photos/cgo_bridge_test_impl.go @@ -78,6 +78,10 @@ func (*CgoBridge) Cancel() { C.photos_request_cancel() } +func (*CgoBridge) IsCancelled() bool { + return C.photos_request_is_cancelled() != 0 +} + func (*CgoBridge) ExportPreview(assetID, outputDir string, targetSize, index int) (ExportResult, error) { cid := C.CString(assetID) defer C.free(unsafe.Pointer(cid))