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
This commit is contained in:
Ein Anderssono
2026-06-11 21:44:55 +02:00
parent 009c71e6bb
commit 479c284dfc
9 changed files with 32 additions and 1 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
BINARY := ./bin/photoscli BINARY := ./bin/photoscli
MODULE := gitea.k3s.k0.nu/tools/photocli MODULE := gitea.k3s.k0.nu/tools/photocli
VERSION := 0.2.3 VERSION := 0.2.4
BRIDGE_DIR := bridge BRIDGE_DIR := bridge
LDFLAGS := -X main.version=$(VERSION) LDFLAGS := -X main.version=$(VERSION)
OBJ := $(BRIDGE_DIR)/photokit_bridge.o OBJ := $(BRIDGE_DIR)/photokit_bridge.o
+2
View File
@@ -28,6 +28,8 @@ char *photos_list_tree_json(void);
void photos_request_cancel(void); void photos_request_cancel(void);
int photos_request_is_cancelled(void);
void photos_free_string(char *value); void photos_free_string(char *value);
#ifdef __cplusplus #ifdef __cplusplus
+4
View File
@@ -415,3 +415,7 @@ void photos_free_string(char *value) {
void photos_request_cancel(void) { void photos_request_cancel(void) {
photos_cancelled = 1; photos_cancelled = 1;
} }
int photos_request_is_cancelled(void) {
return photos_cancelled;
}
+4
View File
@@ -74,6 +74,10 @@ void photos_request_cancel(void) {
stub_cancelled = 1; stub_cancelled = 1;
} }
int photos_request_is_cancelled(void) {
return stub_cancelled;
}
void photos_test_set_export_preview_json(const char *json) { void photos_test_set_export_preview_json(const char *json) {
stub_export_preview_json = json; stub_export_preview_json = json;
} }
+11
View File
@@ -263,6 +263,9 @@ func backupTree(nodes []photos.CollectionNode, outDir string, targetSize int, or
total := 0 total := 0
failed := 0 failed := 0
for _, node := range nodes { for _, node := range nodes {
if bridge.IsCancelled() {
break
}
path := outDir + "/" + sanitizePathComponent(node.Name) path := outDir + "/" + sanitizePathComponent(node.Name)
if node.Kind == "folder" { if node.Kind == "folder" {
n, t, f, err := backupTree(node.Children, path, targetSize, originals, stderr, bridge) 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 exported := 0
failed := 0 failed := 0
for i, a := range assets { for i, a := range assets {
if bridge.IsCancelled() {
break
}
result, exportErr := exportOne(bridge, a, outDir, targetSize, originals, i) result, exportErr := exportOne(bridge, a, outDir, targetSize, originals, i)
progressBar(stderr, exported+failed+1, total, dirPrefix+result.Filename, result.Size, result.Cloud) progressBar(stderr, exported+failed+1, total, dirPrefix+result.Filename, result.Size, result.Cloud)
if exportErr != nil { if exportErr != nil {
@@ -337,6 +343,11 @@ func exportAssetsParallel(assets []photos.Asset, outDir string, targetSize int,
go func() { go func() {
defer wg.Done() defer wg.Done()
for i := range jobs { 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) result, exportErr := exportOne(bridge, assets[i], outDir, targetSize, originals, i)
slots[i].result = result slots[i].result = result
slots[i].err = exportErr slots[i].err = exportErr
+1
View File
@@ -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 return photos.ExportResult{Filename: "test.jpg", Size: 2048, Cloud: "cloud"}, nil
} }
func (m *mockBridge) Cancel() { m.cancelled = true } 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) { func runWith(args []string, b photos.Bridge) (string, string, int) {
var out, err bytes.Buffer var out, err bytes.Buffer
+1
View File
@@ -13,6 +13,7 @@ type Bridge interface {
ExportPreview(assetID, outputDir string, targetSize, index int) (ExportResult, error) ExportPreview(assetID, outputDir string, targetSize, index int) (ExportResult, error)
ExportOriginal(assetID, outputDir string, index int) (ExportResult, error) ExportOriginal(assetID, outputDir string, index int) (ExportResult, error)
Cancel() Cancel()
IsCancelled() bool
} }
func ParseAlbumsJSON(jsonStr string) ([]Album, error) { func ParseAlbumsJSON(jsonStr string) ([]Album, error) {
+4
View File
@@ -58,6 +58,10 @@ func (*CgoBridge) Cancel() {
C.photos_request_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) { func (*CgoBridge) ExportPreview(assetID, outputDir string, targetSize, index int) (ExportResult, error) {
cid := C.CString(assetID) cid := C.CString(assetID)
defer C.free(unsafe.Pointer(cid)) defer C.free(unsafe.Pointer(cid))
+4
View File
@@ -78,6 +78,10 @@ func (*CgoBridge) Cancel() {
C.photos_request_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) { func (*CgoBridge) ExportPreview(assetID, outputDir string, targetSize, index int) (ExportResult, error) {
cid := C.CString(assetID) cid := C.CString(assetID)
defer C.free(unsafe.Pointer(cid)) defer C.free(unsafe.Pointer(cid))