diff --git a/proto/anki/backend.proto b/proto/anki/backend.proto index 74675f4bd..6f3d7c778 100644 --- a/proto/anki/backend.proto +++ b/proto/anki/backend.proto @@ -58,6 +58,8 @@ message BackendError { SEARCH_ERROR = 14; CUSTOM_STUDY_ERROR = 15; IMPORT_ERROR = 16; + // Collection imported, but media import failed. + IMPORT_MEDIA_ERROR = 17; } // localized error description suitable for displaying to the user diff --git a/proto/anki/import_export.proto b/proto/anki/import_export.proto index e21c7ffb4..94711c31a 100644 --- a/proto/anki/import_export.proto +++ b/proto/anki/import_export.proto @@ -33,8 +33,8 @@ message PackageMetadata { VERSION_LEGACY_1 = 1; // When `meta` missing, and collection.anki21 file present. VERSION_LEGACY_2 = 2; - /// Implies MediaEntry media map, and zstd compression. - /// collection.21b file + // Implies MediaEntry media map, and zstd compression. + // collection.21b file VERSION_LATEST = 3; } diff --git a/pylib/anki/_backend/__init__.py b/pylib/anki/_backend/__init__.py index e0084a326..155f09413 100644 --- a/pylib/anki/_backend/__init__.py +++ b/pylib/anki/_backend/__init__.py @@ -24,6 +24,7 @@ from ..errors import ( DBError, ExistsError, FilteredDeckError, + ImportMediaError, Interrupted, InvalidInput, LocalizedError, @@ -219,6 +220,9 @@ def backend_exception_to_pylib(err: backend_pb2.BackendError) -> Exception: elif val == kind.CUSTOM_STUDY_ERROR: return CustomStudyError(err.localized) + elif val == kind.IMPORT_MEDIA_ERROR: + return ImportMediaError(err.localized) + else: # sadly we can't do exhaustiveness checking on protobuf enums # assert_exhaustive(val) diff --git a/pylib/anki/errors.py b/pylib/anki/errors.py index 07e26cc9e..c0b004aba 100644 --- a/pylib/anki/errors.py +++ b/pylib/anki/errors.py @@ -80,6 +80,10 @@ class AbortSchemaModification(Exception): pass +class ImportMediaError(LocalizedError): + pass + + # legacy DeckRenameError = FilteredDeckError AnkiError = AbortSchemaModification diff --git a/qt/aqt/importing.py b/qt/aqt/importing.py index 7e5f65c64..b9ee36d21 100644 --- a/qt/aqt/importing.py +++ b/qt/aqt/importing.py @@ -11,7 +11,7 @@ import anki.importing as importing import aqt.deckchooser import aqt.forms import aqt.modelchooser -from anki.errors import Interrupted +from anki.errors import ImportMediaError, Interrupted from anki.importing.anki2 import V2ImportIntoV1 from anki.importing.apkg import AnkiPackageImporter from aqt import AnkiQt, gui_hooks @@ -457,7 +457,8 @@ def replace_with_apkg( mw: aqt.AnkiQt, filename: str, callback: Callable[[bool], None] ) -> None: """Tries to replace the provided collection with the provided backup, - then calls the callback. True if success. + then calls the callback. True if collection file was imported (even + if media failed). """ dialog = mw.progress.start(immediate=True) timer = QTimer() @@ -495,7 +496,8 @@ def replace_with_apkg( except Exception as error: if not isinstance(error, Interrupted): showWarning(str(error)) - callback(False) + collection_file_imported = isinstance(error, ImportMediaError) + callback(collection_file_imported) else: callback(True) diff --git a/rslib/src/backend/error.rs b/rslib/src/backend/error.rs index 165e1c7fb..4fa4b7513 100644 --- a/rslib/src/backend/error.rs +++ b/rslib/src/backend/error.rs @@ -4,7 +4,7 @@ use crate::{ backend_proto as pb, backend_proto::backend_error::Kind, - error::{AnkiError, SyncErrorKind}, + error::{AnkiError, ImportError, SyncErrorKind}, prelude::*, }; @@ -34,6 +34,7 @@ impl AnkiError { AnkiError::MultipleNotetypesSelected => Kind::InvalidInput, AnkiError::DatabaseCheckRequired => Kind::InvalidInput, AnkiError::CustomStudyError(_) => Kind::CustomStudyError, + AnkiError::ImportError(ImportError::MediaImportFailed(_)) => Kind::ImportMediaError, AnkiError::ImportError(_) => Kind::ImportError, AnkiError::FileIoError(_) => Kind::IoError, AnkiError::MediaCheckRequired => Kind::InvalidInput, diff --git a/rslib/src/backend/import_export.rs b/rslib/src/backend/import_export.rs index 47d99a456..563b31d66 100644 --- a/rslib/src/backend/import_export.rs +++ b/rslib/src/backend/import_export.rs @@ -39,6 +39,7 @@ impl ImportExportService for Backend { &input.backup_path, &input.col_path, &input.media_folder, + &self.tr, self.import_progress_fn(), ) .map(Into::into) diff --git a/rslib/src/import_export/package/colpkg/import.rs b/rslib/src/import_export/package/colpkg/import.rs index efd820e54..1350b56b4 100644 --- a/rslib/src/import_export/package/colpkg/import.rs +++ b/rslib/src/import_export/package/colpkg/import.rs @@ -57,6 +57,7 @@ pub fn import_colpkg( colpkg_path: &str, target_col_path: &str, target_media_folder: &str, + tr: &I18n, mut progress_fn: impl FnMut(ImportProgress) -> Result<()>, ) -> Result<()> { progress_fn(ImportProgress::Collection)?; @@ -76,7 +77,12 @@ pub fn import_colpkg( progress_fn(ImportProgress::Collection)?; let media_folder = Path::new(target_media_folder); - let media_import_result = restore_media(&meta, progress_fn, &mut archive, media_folder); + let media_import_result = restore_media(&meta, progress_fn, &mut archive, media_folder) + .map_err(|err| { + AnkiError::ImportError(ImportError::MediaImportFailed( + err.localized_description(tr), + )) + }); // Proceed with replacing collection, regardless of media import result tempfile.as_file().sync_all()?; diff --git a/rslib/src/import_export/package/colpkg/tests.rs b/rslib/src/import_export/package/colpkg/tests.rs index 37da08c41..b0642f5c2 100644 --- a/rslib/src/import_export/package/colpkg/tests.rs +++ b/rslib/src/import_export/package/colpkg/tests.rs @@ -40,6 +40,7 @@ fn roundtrip() -> Result<()> { for (legacy, name) in [(true, "legacy"), (false, "v3")] { // export to a file let col = collection_with_media(dir, name)?; + let tr = col.tr.clone(); let colpkg_name = dir.join(format!("{name}.colpkg")); col.export_colpkg(&colpkg_name, true, legacy, |_| ())?; // import into a new collection @@ -52,6 +53,7 @@ fn roundtrip() -> Result<()> { &colpkg_name.to_string_lossy(), &anki2_name, import_media_dir.to_str().unwrap(), + &tr, |_| Ok(()), )?; // confirm collection imported