Datenbank Import + export

Diese Seite verwendet Cookies. Durch die Nutzung unserer Seite erklären Sie sich damit einverstanden, dass wir Cookies setzen. Weitere Informationen

  • Datenbank Import + export

    Hallo zusammen,
    ich verwalte in meiner App eine Datenbank. Ich möchte der App gerne die Funktion zum Sport und Export der Datenbank hinzufügen. Der Export funktioniert soweit erstmal, es wird eine *.sqlite Datei erstellt. Nun habe ich beim Import das Problem, dass ich die Datei zwar im Fester sehe, ich kann jedoch keine Datei auswählen. Woran hängt? Muss ich der App den Zugriff noch extra in der Info erlauben? Ist mein Code unvollständig?
    Anbei der Code, wie ich die Funktion aktuell implementiert habe

    Quellcode

    1. // export funktion
    2. func exportDatabase(completion: @escaping (URL?) -> Void) {
    3. let context = persistentContainer.viewContext
    4. let storeCoordinator = persistentContainer.persistentStoreCoordinator
    5. guard let store = storeCoordinator.persistentStores.first else {
    6. completion(nil)
    7. return
    8. }
    9. let exportURL = FileManager.default.temporaryDirectory.appendingPathComponent("BookCatalog.sqlite")
    10. do {
    11. try storeCoordinator.migratePersistentStore(store, to: exportURL, options: nil, withType: NSSQLiteStoreType)
    12. completion(exportURL)
    13. } catch {
    14. print("Failed to export database: \(error)")
    15. completion(nil)
    16. }
    17. }
    18. // import funktion
    19. func importDatabase(from url: URL, completion: @escaping (Bool) -> Void) {
    20. let storeCoordinator = persistentContainer.persistentStoreCoordinator
    21. guard let store = storeCoordinator.persistentStores.first else {
    22. completion(false)
    23. return
    24. }
    25. do {
    26. try storeCoordinator.destroyPersistentStore(at: store.url!, ofType: NSSQLiteStoreType, options: nil)
    27. try storeCoordinator.replacePersistentStore(at: store.url!, destinationOptions: nil, withPersistentStoreFrom: url, sourceOptions: nil, ofType: NSSQLiteStoreType)
    28. try persistentContainer.viewContext.save()
    29. completion(true)
    30. } catch {
    31. print("Failed to import database: \(error)")
    32. completion(false)
    33. }
    34. }
    Alles anzeigen

    Und der Aufruf

    Quellcode

    1. // Export and Import Section
    2. DisclosureGroup {
    3. VStack {
    4. Button("Export Database") {
    5. databaseManager.exportDatabase { url in
    6. if let url = url {
    7. documentURL = url
    8. showDocumentExporter = true
    9. }
    10. }
    11. }
    12. .padding()
    13. .background(Color.blue)
    14. .foregroundColor(.white)
    15. .cornerRadius(8)
    16. .fileExporter(isPresented: $showDocumentExporter, document: documentURL.map { DatabaseDocument(url: $0) }, contentType: .database, defaultFilename: "BookCatalog") { result in
    17. switch result {
    18. case .success:
    19. print("Export successful")
    20. case .failure(let error):
    21. print("Export failed: \(error)")
    22. }
    23. }
    24. Button("Import Database") {
    25. showDocumentImporter = true
    26. }
    27. .padding()
    28. .background(Color.green)
    29. .foregroundColor(.white)
    30. .cornerRadius(8)
    31. .fileImporter(isPresented: $showDocumentImporter, allowedContentTypes: [.database], allowsMultipleSelection: false) { result in
    32. switch result {
    33. case .success(let urls):
    34. if let url = urls.first {
    35. databaseManager.importDatabase(from: url) { success in
    36. if success {
    37. showAlert = true
    38. } else {
    39. print("Import failed")
    40. }
    41. }
    42. }
    43. case .failure(let error):
    44. print("Import failed: \(error)")
    45. }
    46. }
    47. }
    48. }
    49. label: {
    50. Text("Export/Import Database")
    51. .foregroundColor(.blue)
    52. }
    53. }
    54. .padding()
    Alles anzeigen
  • Menslo schrieb:

    .fileImporter(isPresented: $showDocumentImporter, allowedContentTypes: [.database], allowsMultipleSelection: false) { result in
    Dein Problem dürfte im hier verwendeten UTType liegen: Ich vermute, dass die von Dir erstellte Datei nicht dem Content-Type public.database entspricht, der hier als einziger erlaubt ist - das könntest Du einmal mittels mdls prüfen. Im Zweifelsfall müsstest Du in der info.plist einen entsprechenden UTI definieren...

    Mattes
    Diese Seite bleibt aus technischen Gründen unbedruckt.
  • Menslo schrieb:

    Kann ich steuern, welchen Content Type die Datei entspricht (Fundwie?)?
    Schau mal in den verlinkten Artikel bzgl. UTIs und deren Definition in der info.plist. Ich würde erst einmal den Content Type besagter Datei überprüfen ... dann weisst Du auch, welchen Du beim OpenPanel erlauben musst: Eventuell ist die Definition eines eigenen UTI gar nicht erforderlich.

    Eigentlich nichts, was ich nicht oben schon erwähnte ;)

    Mattes
    Diese Seite bleibt aus technischen Gründen unbedruckt.
  • Welche UTI ist es denn? Du exportierst nur welche, die Du selber "verantwortest" (meist proprietäre Dateiformate) - hier kaum der Fall. Eventuell reicht es schon, den Identifier nur in Deinem Aufruf des OpenPanels als akzeptierten Dateityp anzugeben. Sonst eben in der info.plist importieren.

    Der oben verlinkte Artikel führt Dich nach zwei Klicks zu Hintergrundmaterial, das Dir hilft:

    Apple schrieb:

    Define an exported type when your app is the canonical source of information for that type. For example, if your app uses its own proprietary document format, declare it as an exported type.
    Define an imported type if your app uses a type that another app defines, or if it’s a proprietary file format the system doesn’t declare. When importing a type from another app, don’t declare your own identifier; instead, use the same type identifier as the original.
    Mattes
    Diese Seite bleibt aus technischen Gründen unbedruckt.
  • Die Überprüfung mit mdls ergibt

    Quellcode

    1. kMDItemContentTypeTree = (
    2. "public.item",
    3. "dyn.ah62d4rv4ge81g6pqrf4gn",
    4. "public.data"
    5. )
    Ich bin ein bisschen überfragt beim Hinzufügen des Arrays in der info, welche Werte ich dort angeben muss. Vor Allem bei den Additional document type Properties

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von Menslo ()

  • Menslo schrieb:

    Ich bin ein bisschen überfragt beim Hinzufügen des Arrays in der info, welche Werte ich dort angeben muss. Vor Allem bei den Additional document type Properties
    Geh' mal einen Schritt zurück:

    Du speicherst eine Datei, die Du wieder öffnen möchtest. Diese Datei hat nur eine sehr generische UTI (bzw. Konformität zu sehr allgemeinen Typen). Du hast also zwei Optionen:
    1. Du erlaubst beim Öffnen auch alle Dateien der UTIs "public.item" bzw. "public.data". Damit kann der Benutzer diverse Dateien selektieren, die eventuell gar keinen Sinn machen. Deine App muss unerwartete Dateiformate händeln - sollte sie aber eh.
    2. Du definierst einen eigenen UTI, entweder auf Basis der Dateierweiterung ".sqlite" oder einer eigenen, den Du in der info.plist exportierst. Diesen UTI kannst Du dann beim Open referenzieren. Hier findest Du die entsprechenden Keys erklärt - oder schau einmal in bestehende Apps.
    Ich würde pragmatisch zunächst den ersten Weg probieren - nur bei für die App essentiellen Dateitypen (z. B. Dokument-Bearbeitung, externes Öffnen etc.) sind eigene UTIs m. E. unumgänglich.

    Das Thema erfordert etwas Recherche und Testen, insbesondere wenn Konflikte mit exportierten UTIs anderer Apps auftreten - es lohnt sich aber, tiefer einzusteigen.

    Mattes
    Diese Seite bleibt aus technischen Gründen unbedruckt.
  • Ich melde mich hier nochmal zurück :)

    Vielen Dank Mattes, ich habe es durch deine Ausführungen geschafft, dass ich die Datei auswählen kann.
    Ich habe den importer bzw den Exporter angepasst

    Quellcode

    1. .fileExporter(isPresented: $showExportDocumentPicker, document: documentURL.map { DatabaseDocument(url: $0) }, contentType: .data, defaultFilename: "BookCatalog") { result in
    2. switch result {
    3. case .success:
    4. print("Export successful")
    5. case .failure(let error):
    6. print("Export failed: \(error)")
    7. }
    8. }
    9. .fileImporter(isPresented: $showImportDocumentPicker, allowedContentTypes: [.item, .data], allowsMultipleSelection: false) { result in
    10. switch result {
    11. case .success(let urls):
    12. if let url = urls.first {
    13. databaseManager.importDatabase(from: url) { success in
    14. if success {
    15. showAlert = true
    16. } else {
    17. print("Import failed")
    18. }
    19. }
    20. }
    21. case .failure(let error):
    22. print("Import failed: \(error)")
    23. }
    24. }
    Alles anzeigen



    Jetzt kann ich die Datei zwar wählen, der Import funktioniert aber nicht. Scheinbar habe ich nicht die Berechtigungen auf die Datei zuzugreifen. In der Konsole kommt die Fehlermeldung


    Quellcode

    1. Selected URL: file:///private/var/mobile/Containers/Shared/AppGroup/0EB2C7E0-AB28-4802-BA37-E763DC319E1D/File%20Provider%20Storage/BookCatalog.sqlite
    2. Failed to import database: Error Domain=NSCocoaErrorDomain Code=257 "Die Datei „BookCatalog.sqlite“ konnte nicht geöffnet werden, da du nicht die Zugriffsrechte hast, um sie anzuzeigen." UserInfo={NSFilePath=/private/var/mobile/Containers/Shared/AppGroup/0EB2C7E0-AB28-4802-BA37-E763DC319E1D/File Provider Storage/BookCatalog.sqlite, NSUnderlyingError=0x281ff3a50 {Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted"}}
    3. Import failed
    Wie schaffe ich es, der App den Zugriff zu erlauben?
  • Menslo schrieb:

    Wie schaffe ich es, der App den Zugriff zu erlauben?
    Nach meinem Verständnis wird nach der Bestätigung des Open-Panels die Sandbox Deiner App um Rechte auf die gewählte Datei erweitert. Allerdings bin ich mir nicht sicher, ob dass auch für App-Container gilt. Ist Deine App denn Teil der genannten Application-Group? Mir kommt der Dateipfad etwas dubios vor ("File Provider Storage"?). Vielleicht testest Du erstmal mit Dateien, die in normalen Verzeichnissen liegen?

    Außerdem haben meine Apps ein solches Entitlement, das würde ich mal prüfen:

    Quellcode

    1. <key>com.apple.security.files.user-selected.read-write</key>
    2. <true/>
    Auf jeden Fall scheinst Du ein Problem mit dem Sandboxing zu haben.
    Diese Seite bleibt aus technischen Gründen unbedruckt.
  • Das Vorhandensein des com.apple.security.files.user-selected.read-write Entitlements vorausgesetzt, musst du beim Verwenden des SwiftUI-FileImporters zusätzlich jeden Zugriff auf eine zurückgelieferte URL mit startAccessingSecurityScopedResource/stopAccessingSecurityScopedResource an- und wieder abmelden.

    Quellcode

    1. // ...
    2. if let url = urls.first {
    3. defer {
    4. url.stopAccessingSecurityScopedResource()
    5. }
    6. guard url.startAccessingSecurityScopedResource() else {
    7. // Fehler behandeln
    8. }
    9. databaseManager.importDatabase(from: url) { success in
    10. if success {
    11. showAlert = true
    12. } else {
    13. print("Import failed")
    14. }
    15. }
    16. }
    17. // ...
    Alles anzeigen
  • Sooo - vielen Dank euch beiden. Es funktioniert jetzt.


    Quellcode

    1. func importDatabase(from url: URL, completion: @escaping (Bool) -> Void) {
    2. let fileManager = FileManager.default
    3. guard let storeURL = persistentContainer.persistentStoreCoordinator.persistentStores.first?.url else {
    4. print("Failed to get store URL")
    5. completion(false)
    6. return
    7. }
    8. // Temporärer Speicherort in der App's Dokumentenverzeichnis
    9. let tempDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
    10. let tempFileURL = tempDirectory.appendingPathComponent("temp_imported_database.sqlite")
    11. // Start accessing the security-scoped resource
    12. guard url.startAccessingSecurityScopedResource() else {
    13. print("Failed to access security scoped resource")
    14. completion(false)
    15. return
    16. }
    17. defer {
    18. // Stop accessing the security-scoped resource
    19. url.stopAccessingSecurityScopedResource()
    20. }
    21. do {
    22. // Entfernen der temporären Datei, falls sie bereits existiert
    23. if fileManager.fileExists(atPath: tempFileURL.path) {
    24. try fileManager.removeItem(at: tempFileURL)
    25. }
    26. // Kopieren der Datei an einen temporären Speicherort
    27. try fileManager.copyItem(at: url, to: tempFileURL)
    28. // Testen, ob die temporäre Datei ordnungsgemäß funktioniert
    29. let tempPersistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: persistentContainer.managedObjectModel)
    30. try tempPersistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: tempFileURL, options: nil)
    31. // Wenn erfolgreich, entferne die vorhandene Datenbank
    32. if fileManager.fileExists(atPath: storeURL.path) {
    33. try persistentContainer.persistentStoreCoordinator.destroyPersistentStore(at: storeURL, ofType: NSSQLiteStoreType, options: nil)
    34. try fileManager.removeItem(at: storeURL)
    35. }
    36. // Kopiere die temporäre Datei an das endgültige Ziel
    37. try fileManager.copyItem(at: tempFileURL, to: storeURL)
    38. try persistentContainer.persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: nil)
    39. // Lösche die temporäre Datei nach erfolgreichem Kopieren
    40. try fileManager.removeItem(at: tempFileURL)
    41. completion(true)
    42. } catch {
    43. print("Failed to import database: \(error)")
    44. completion(false)
    45. }
    46. }
    Alles anzeigen
    Einzig eine Frage zu den Entitlements habe ich noch. Das habe ich jetzt noch nicht hinzugefügt, weil ich nicht herausgefunden habe wie :rolleyes: Bei Signing & Capabilities kann ich App Sandbox nicht auswählen
    [Blockierte Grafik: https://ibb.co/KFwYq1N]
  • Kannst eigentlich schon so machen, musst diese halt nur in einen geschützten bereich importieren und dann von dort in die Datenbank konvertiert updaten.

    Ich selbst verwende hierfür eine komprimierte JSON, bei welcher ich einen Merker mitlaufen lasse, welcher die DB Version angibt. In Abhängigkeit dieser und der aktuellen DB Version werden dann entsprechende Konvertierungen angestoßen oder Fehlermeldungen ausgegeben,

    Und sage nie, nie... es ist meist nur eine Frage der Zeit, bis sich die Datenbank weiterentwickelt.... daher würde ich von haus aus schon einen entsprechenden Mechanismus einbauen, auch wenn er nur eine Fehlermeldung bei inkompatibelität ausgibt...
  • Wolf schrieb:

    Ich selbst verwende hierfür eine komprimierte JSON, bei welcher ich einen Merker mitlaufen lasse, welcher die DB Version angibt.
    Kann ich nur unterstützen! Selbst, wenn - wie im Falle von Core Data - halbwegs intelligente Methoden für die Migration zwischen Datenmodellen existieren: Spätestens beim Restore eines "Vorversionen-Backups" wird es eng ... und falls die App aus irgendeinem Grund unbenutzbar wird, hast Du mit JSON eine sehr generische Schnittstelle.

    BTDT, Mattes
    Diese Seite bleibt aus technischen Gründen unbedruckt.