From 7c8a6d0666e95611515c16883cc6ac46ab212468 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Sat, 14 Jun 2025 09:30:15 +0000 Subject: [PATCH] fix: improve Android file export with version-aware storage handling - Add Android version detection from user agent - Implement tiered storage strategy for Android 10+ restrictions - Enhance directory creation logic with better error handling - Add comprehensive fallback chain for file saving - Improve permission denial handling for graceful degradation - Add structured logging for better debugging Resolves "Parent folder doesn't exist" errors on Android 10+ Ensures files are always saved with multiple fallback levels --- package-lock.json | 323 ++++++++++---- src/components/BackupFilesList.vue | 17 +- .../platforms/CapacitorPlatformService.ts | 396 ++++++++++++++++-- 3 files changed, 608 insertions(+), 128 deletions(-) diff --git a/package-lock.json b/package-lock.json index 18a623a7..f1f8f5f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6983,6 +6983,29 @@ "integrity": "sha512-meL9DERHj+fFVWoOX9fXqfcYcSpUfSYJPcFvDPKrxitICbwAoWR+Ut4j5NO9zAT917HUHLQmqzQbAsGNHlDcxQ==", "license": "Apache-2.0 OR MIT" }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -7839,9 +7862,9 @@ } }, "node_modules/@react-native/assets-registry": { - "version": "0.79.3", - "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.3.tgz", - "integrity": "sha512-Vy8DQXCJ21YSAiHxrNBz35VqVlZPpRYm50xRTWRf660JwHuJkFQG8cUkrLzm7AUriqUXxwpkQHcY+b0ibw9ejQ==", + "version": "0.80.0", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.80.0.tgz", + "integrity": "sha512-MlScsKAz99zoYghe5Rf5mUqsqz2rMB02640NxtPtBMSHNdGxxRlWu/pp1bFexDa1DYJwyIjnLgt3Z/Y90ikHfw==", "license": "MIT", "optional": true, "peer": true, @@ -7947,20 +7970,20 @@ } }, "node_modules/@react-native/community-cli-plugin": { - "version": "0.79.3", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.79.3.tgz", - "integrity": "sha512-N/+p4HQqN4yK6IRzn7OgMvUIcrmEWkecglk1q5nj+AzNpfIOzB+mqR20SYmnPfeXF+mZzYCzRANb3KiM+WsSDA==", + "version": "0.80.0", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.80.0.tgz", + "integrity": "sha512-uadfVvzZfz5tGpqwslL12i+rELK9m6cLhtqICX0JQvS7Bu12PJwrozhKzEzIYwN9i3wl2dWrKDUr08izt7S9Iw==", "license": "MIT", "optional": true, "peer": true, "dependencies": { - "@react-native/dev-middleware": "0.79.3", + "@react-native/dev-middleware": "0.80.0", "chalk": "^4.0.0", - "debug": "^2.2.0", + "debug": "^4.4.0", "invariant": "^2.2.4", - "metro": "^0.82.0", - "metro-config": "^0.82.0", - "metro-core": "^0.82.0", + "metro": "^0.82.2", + "metro-config": "^0.82.2", + "metro-core": "^0.82.2", "semver": "^7.1.3" }, "engines": { @@ -7975,25 +7998,97 @@ } } }, + "node_modules/@react-native/community-cli-plugin/node_modules/@react-native/debugger-frontend": { + "version": "0.80.0", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.80.0.tgz", + "integrity": "sha512-lpu9Z3xtKUaKFvEcm5HSgo1KGfkDa/W3oZHn22Zy0WQ9MiOu2/ar1txgd1wjkoNiK/NethKcRdCN7mqnc6y2mA==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/@react-native/dev-middleware": { + "version": "0.80.0", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.80.0.tgz", + "integrity": "sha512-lLyTnJ687A5jF3fn8yR/undlCis3FG+N/apQ+Q0Lcl+GV6FsZs0U5H28YmL6lZtjOj4TLek6uGPMPmZasHx7cQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.80.0", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "serve-static": "^1.16.2", + "ws": "^6.2.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@react-native/community-cli-plugin/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "optional": true, "peer": true, "dependencies": { - "ms": "2.0.0" + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/@react-native/community-cli-plugin/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT", "optional": true, "peer": true }, + "node_modules/@react-native/community-cli-plugin/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, "node_modules/@react-native/debugger-frontend": { "version": "0.79.3", "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.79.3.tgz", @@ -8078,9 +8173,9 @@ } }, "node_modules/@react-native/gradle-plugin": { - "version": "0.79.3", - "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.79.3.tgz", - "integrity": "sha512-imfpZLhNBc9UFSzb/MOy2tNcIBHqVmexh/qdzw83F75BmUtLb/Gs1L2V5gw+WI1r7RqDILbWk7gXB8zUllwd+g==", + "version": "0.80.0", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.80.0.tgz", + "integrity": "sha512-drmS68rabSMOuDD+YsAY2luNT8br82ycodSDORDqAg7yWQcieHMp4ZUOcdOi5iW+JCqobablT/b6qxcrBg+RaA==", "license": "MIT", "optional": true, "peer": true, @@ -8089,9 +8184,9 @@ } }, "node_modules/@react-native/js-polyfills": { - "version": "0.79.3", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.79.3.tgz", - "integrity": "sha512-PEBtg6Kox6KahjCAch0UrqCAmHiNLEbp2SblUEoFAQnov4DSxBN9safh+QSVaCiMAwLjvNfXrJyygZz60Dqz3Q==", + "version": "0.80.0", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.80.0.tgz", + "integrity": "sha512-dMX7IcBuwghySTgIeK8q03tYz/epg5ScGmJEfBQAciuhzMDMV1LBR/9wwdgD73EXM/133yC5A+TlHb3KQil4Ew==", "license": "MIT", "optional": true, "peer": true, @@ -8108,9 +8203,9 @@ "peer": true }, "node_modules/@react-native/virtualized-lists": { - "version": "0.79.3", - "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.3.tgz", - "integrity": "sha512-/0rRozkn+iIHya2vnnvprDgT7QkfI54FLrACAN3BLP7MRlfOIGOrZsXpRLndnLBVnjNzkcre84i1RecjoXnwIA==", + "version": "0.80.0", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.80.0.tgz", + "integrity": "sha512-d9zZdPS/ZRexVAkxo1eRp85U7XnnEpXA1ZpSomRKxBuStYKky1YohfEX5YD5MhphemKK24tT7JR4UhaLlmeX8Q==", "license": "MIT", "optional": true, "peer": true, @@ -8874,9 +8969,9 @@ } }, "node_modules/@stencil/core": { - "version": "4.33.1", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.33.1.tgz", - "integrity": "sha512-12k9xhAJBkpg598it+NRmaYIdEe6TSnsL/v6/KRXDcUyTK11VYwZQej2eHnMWtqot+znJ+GNTqb5YbiXi+5Low==", + "version": "4.35.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.35.0.tgz", + "integrity": "sha512-x0IFtj7IJStK+ZqIkhReWbiC0UMjMJnNXV8OXG+DCLDExZaVaxL3MLuq6BJBBcQ1MHZduTHDv3Iz0Zshoj3zjQ==", "license": "MIT", "bin": { "stencil": "bin/stencil" @@ -11184,13 +11279,13 @@ } }, "node_modules/app-builder-lib/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { "node": "20 || >=22" @@ -12868,9 +12963,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001722", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001722.tgz", - "integrity": "sha512-DCQHBBZtiK6JVkAGw7drvAMK0Q0POD/xZvEmDp6baiMMP6QXXk9HpD6mNYBZWhOPG6LvIDb82ITqtWjhDckHCA==", + "version": "1.0.30001723", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz", + "integrity": "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==", "devOptional": true, "funding": [ { @@ -15347,9 +15442,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.166", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.166.tgz", - "integrity": "sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==", + "version": "1.5.167", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.167.tgz", + "integrity": "sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==", "devOptional": true, "license": "ISC" }, @@ -16079,9 +16174,9 @@ } }, "node_modules/ethers": { - "version": "6.14.3", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.14.3.tgz", - "integrity": "sha512-qq7ft/oCJohoTcsNPFaXSQUm457MA5iWqkf1Mb11ujONdg7jBI6sAOrHaTi3j0CBqIGFSCeR/RMc+qwRRub7IA==", + "version": "6.14.4", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.14.4.tgz", + "integrity": "sha512-Jm/dzRs2Z9iBrT6e9TvGxyb5YVKAPLlpna7hjxH7KH/++DSh2T/JVmQUv7iHI5E55hDbp/gEVvstWYXVxXFzsA==", "funding": [ { "type": "individual", @@ -16178,21 +16273,21 @@ "license": "MIT" }, "node_modules/ethr-did": { - "version": "3.0.37", - "resolved": "https://registry.npmjs.org/ethr-did/-/ethr-did-3.0.37.tgz", - "integrity": "sha512-L9UUhAS8B1T7jTRdKLwAt514lx2UrJebJK7uc6UU4AJ9RhY8Vcfwc93Ux82jREE7yvvqDPXsVNH+lS3aw18a9A==", + "version": "3.0.38", + "resolved": "https://registry.npmjs.org/ethr-did/-/ethr-did-3.0.38.tgz", + "integrity": "sha512-gUxtErXVOQUJf+bmnxRdSJdlU9aFbQSBNaJCYGt+PLqw6l4qqInTfMRiWpwe/brhRtdjE+64tnayOVk8ataeQA==", "license": "Apache-2.0", "dependencies": { "did-jwt": "^8.0.0", "did-resolver": "^4.1.0", "ethers": "^6.8.1", - "ethr-did-resolver": "11.0.3" + "ethr-did-resolver": "11.0.4" } }, "node_modules/ethr-did-resolver": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/ethr-did-resolver/-/ethr-did-resolver-11.0.3.tgz", - "integrity": "sha512-lQ1T/SZfgR6Kp05/GSIXnMELxQ5H6M6OCTH4wBTVSAgHzbJiDNVIYWzg/c+NniIM88B0ViAi4CaiCHaiUlvPQg==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/ethr-did-resolver/-/ethr-did-resolver-11.0.4.tgz", + "integrity": "sha512-EJ/dL2QsFzvhBJd0nlPFjma3bxpQOWyp2TytQZyAeqi6SfZ4ALCB0VaA4dSeT4T8ZtI2pzs/sD7t/7A0584J6Q==", "license": "Apache-2.0", "dependencies": { "did-resolver": "^4.1.0", @@ -24054,9 +24149,9 @@ } }, "node_modules/postcss": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", - "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz", + "integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==", "funding": [ { "type": "opencollective", @@ -24954,44 +25049,43 @@ "peer": true }, "node_modules/react-native": { - "version": "0.79.3", - "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.79.3.tgz", - "integrity": "sha512-EzH1+9gzdyEo9zdP6u7Sh3Jtf5EOMwzy+TK65JysdlgAzfEVfq4mNeXcAZ6SmD+CW6M7ARJbvXLyTD0l2S5rpg==", + "version": "0.80.0", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.80.0.tgz", + "integrity": "sha512-b9K1ygb2MWCBtKAodKmE3UsbUuC29Pt4CrJMR0ocTA8k+8HJQTPleBPDNKL4/p0P01QO9aL/gZUddoxHempLow==", "license": "MIT", "optional": true, "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", - "@react-native/assets-registry": "0.79.3", - "@react-native/codegen": "0.79.3", - "@react-native/community-cli-plugin": "0.79.3", - "@react-native/gradle-plugin": "0.79.3", - "@react-native/js-polyfills": "0.79.3", - "@react-native/normalize-colors": "0.79.3", - "@react-native/virtualized-lists": "0.79.3", + "@react-native/assets-registry": "0.80.0", + "@react-native/codegen": "0.80.0", + "@react-native/community-cli-plugin": "0.80.0", + "@react-native/gradle-plugin": "0.80.0", + "@react-native/js-polyfills": "0.80.0", + "@react-native/normalize-colors": "0.80.0", + "@react-native/virtualized-lists": "0.80.0", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", - "babel-plugin-syntax-hermes-parser": "0.25.1", + "babel-plugin-syntax-hermes-parser": "0.28.1", "base64-js": "^1.5.1", "chalk": "^4.0.0", "commander": "^12.0.0", - "event-target-shim": "^5.0.1", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", - "metro-runtime": "^0.82.0", - "metro-source-map": "^0.82.0", + "metro-runtime": "^0.82.2", + "metro-source-map": "^0.82.2", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.1", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", - "scheduler": "0.25.0", + "scheduler": "0.26.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", @@ -25005,8 +25099,8 @@ "node": ">=18" }, "peerDependencies": { - "@types/react": "^19.0.0", - "react": "^19.0.0" + "@types/react": "^19.1.0", + "react": "^19.1.0" }, "peerDependenciesMeta": { "@types/react": { @@ -25039,14 +25133,46 @@ "react-native": "*" } }, + "node_modules/react-native/node_modules/@react-native/codegen": { + "version": "0.80.0", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.80.0.tgz", + "integrity": "sha512-X9TsPgytoUkNrQjzAZh4dXa4AuouvYT0NzYyvnjw1ry4LESCZtKba+eY4x3+M30WPR52zjgu+UFL//14BSdCCA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "glob": "^7.1.1", + "hermes-parser": "0.28.1", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, "node_modules/react-native/node_modules/@react-native/normalize-colors": { - "version": "0.79.3", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.3.tgz", - "integrity": "sha512-T75NIQPRFCj6DFMxtcVMJTZR+3vHXaUMSd15t+CkJpc5LnyX91GVaPxpRSAdjFh7m3Yppl5MpdjV/fntImheYQ==", + "version": "0.80.0", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.80.0.tgz", + "integrity": "sha512-bJZDSopadjJxMDvysc634eTfLL4w7cAx5diPe14Ez5l+xcKjvpfofS/1Ja14DlgdMJhxGd03MTXlrxoWust3zg==", "license": "MIT", "optional": true, "peer": true }, + "node_modules/react-native/node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.28.1.tgz", + "integrity": "sha512-meT17DOuUElMNsL5LZN56d+KBp22hb0EfxWfuPUeoSi54e40v1W4C2V36P75FpsH9fVEfDKpw5Nnkahc8haSsQ==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "hermes-parser": "0.28.1" + } + }, "node_modules/react-native/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -25058,6 +25184,25 @@ "node": ">=18" } }, + "node_modules/react-native/node_modules/hermes-estree": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.28.1.tgz", + "integrity": "sha512-w3nxl/RGM7LBae0v8LH2o36+8VqwOZGv9rX1wyoWT6YaKZLqpJZ0YQ5P0LVr3tuRpf7vCx0iIG4i/VmBJejxTQ==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/react-native/node_modules/hermes-parser": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.28.1.tgz", + "integrity": "sha512-nf8o+hE8g7UJWParnccljHumE9Vlq8F7MqIdeahl+4x0tvCUJYRrT0L7h0MMg/X9YJmkNwsfbaNNrzPtFXOscg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "hermes-estree": "0.28.1" + } + }, "node_modules/react-native/node_modules/ws": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", @@ -25960,15 +26105,15 @@ } }, "node_modules/rimraf/node_modules/glob": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", - "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "dev": true, "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" @@ -26010,13 +26155,13 @@ } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { "node": "20 || >=22" @@ -26410,9 +26555,9 @@ "license": "ISC" }, "node_modules/scheduler": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", - "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT", "optional": true, "peer": true @@ -31045,9 +31190,9 @@ } }, "node_modules/zod": { - "version": "3.25.61", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.61.tgz", - "integrity": "sha512-fzfJgUw78LTNnHujj9re1Ov/JJQkRZZGDMcYqSx7Hp4rPOkKywaFHq0S6GoHeXs0wGNE/sIOutkXgnwzrVOGCQ==", + "version": "3.25.64", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.64.tgz", + "integrity": "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/src/components/BackupFilesList.vue b/src/components/BackupFilesList.vue index 00078c2e..c084845f 100644 --- a/src/components/BackupFilesList.vue +++ b/src/components/BackupFilesList.vue @@ -268,14 +268,25 @@ export default class BackupFilesList extends Vue { return true; } catch (error) { logger.error('[BackupFilesList] Storage permission denied:', error); + + // Get specific guidance for the platform + let guidance = "This app needs permission to access your files to list and restore backups."; + if (typeof platformService.getStoragePermissionGuidance === 'function') { + try { + guidance = await platformService.getStoragePermissionGuidance(); + } catch (guidanceError) { + logger.warn('[BackupFilesList] Could not get permission guidance:', guidanceError); + } + } + this.$notify( { group: "alert", - type: "danger", + type: "warning", title: "Storage Permission Required", - text: "This app needs permission to access your files to list and restore backups. Please grant storage permission and try again.", + text: guidance, }, - 7000, + 10000, // Show for 10 seconds to give user time to read ); return false; } diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index b1a5f5f3..9f91732e 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -304,7 +304,8 @@ export class CapacitorPlatformService implements PlatformService { if ( err.message.includes("permission") || err.message.includes("access") || - err.message.includes("denied") + err.message.includes("denied") || + err.message.includes("User denied storage permission") ) { logger.log( "Permission check failed, requesting permissions", @@ -325,6 +326,20 @@ export class CapacitorPlatformService implements PlatformService { return; } catch (retryError: unknown) { const retryErr = retryError as Error; + + // If permission is still denied, log it but don't throw an error + // This allows the app to continue with limited functionality + if (retryErr.message.includes("User denied storage permission")) { + logger.warn( + "External storage permissions denied by user, continuing with limited functionality", + JSON.stringify({ + error: retryErr.message, + timestamp: new Date().toISOString() + }, null, 2), + ); + return; // Don't throw error, just return + } + throw new Error( `Failed to obtain external storage permissions: ${retryErr.message}`, ); @@ -688,6 +703,113 @@ export class CapacitorPlatformService implements PlatformService { } } + /** + * Ensures a directory exists by creating it if necessary. + * This is a workaround for Android's Filesystem.writeFile recursive option not working reliably. + * @param path - The directory path to ensure exists + * @param directory - The base directory type + * @returns Promise that resolves when the directory is ready + */ + private async ensureDirectoryExists(path: string, directory: Directory): Promise { + try { + // Try to read the directory to see if it exists + await Filesystem.readdir({ + path, + directory, + }); + logger.log("[CapacitorPlatformService] Directory already exists:", { + path, + directory, + timestamp: new Date().toISOString(), + }); + } catch (error) { + // Directory doesn't exist, try to create it + const errorMessage = error instanceof Error ? error.message : String(error); + logger.log("[CapacitorPlatformService] Directory doesn't exist, attempting to create:", { + path, + directory, + error: errorMessage, + timestamp: new Date().toISOString(), + }); + + try { + // For Android 10+, we need to handle storage restrictions differently + if (!this.getCapabilities().isIOS) { + // Try creating the directory by writing a temporary file with recursive=true + const tempFileName = `.temp-${Date.now()}`; + const tempPath = `${path}/${tempFileName}`; + + await Filesystem.writeFile({ + path: tempPath, + data: "", + directory, + encoding: Encoding.UTF8, + recursive: true, + }); + + // Clean up the temporary file, leaving the directory + try { + await Filesystem.deleteFile({ + path: tempPath, + directory, + }); + } catch (deleteError) { + // Ignore delete errors - the directory was created successfully + const deleteErrorMessage = deleteError instanceof Error ? deleteError.message : String(deleteError); + logger.log("[CapacitorPlatformService] Temporary file cleanup failed (non-critical):", { + tempPath, + error: deleteErrorMessage, + timestamp: new Date().toISOString(), + }); + } + + logger.log("[CapacitorPlatformService] Directory created successfully:", { + path, + directory, + method: "temporary_file", + timestamp: new Date().toISOString(), + }); + } else { + // For iOS, use the standard approach + const tempFileName = `.temp-${Date.now()}`; + const tempPath = `${path}/${tempFileName}`; + + await Filesystem.writeFile({ + path: tempPath, + data: "", + directory, + encoding: Encoding.UTF8, + recursive: true, + }); + + await Filesystem.deleteFile({ + path: tempPath, + directory, + }); + + logger.log("[CapacitorPlatformService] Directory created successfully:", { + path, + directory, + method: "temporary_file", + timestamp: new Date().toISOString(), + }); + } + } catch (createError) { + const createErrorMessage = createError instanceof Error ? createError.message : String(createError); + logger.warn("[CapacitorPlatformService] Failed to create directory, will try without it:", { + path, + directory, + error: createErrorMessage, + timestamp: new Date().toISOString(), + }); + + // For Android 10+, some directories may not be accessible + // We'll let the calling method handle the fallback + throw createError; + } + } + } + /** * Saves a file directly to user-accessible storage that persists between installations. * On Android: Saves to external storage (Downloads or app-specific directory) @@ -707,57 +829,97 @@ export class CapacitorPlatformService implements PlatformService { encoding: Encoding.UTF8, }); - logger.log("[CapacitorPlatformService] File saved to iOS Documents (persistent):", { - uri: result.uri, + logger.log("[CapacitorPlatformService] File saved to iOS Documents:", { fileName, - note: "File persists between app installations and is accessible via Files app", + uri: result.uri, timestamp: new Date().toISOString(), }); return result.uri; } else { - // Android: Save to external storage that persists between installations - // Try to save to Downloads first, then fallback to app's external storage + // Android: Check for storage restrictions + const hasRestrictions = await this.hasStorageRestrictions(); + const androidVersion = await this.getAndroidVersion(); + + logger.log("[CapacitorPlatformService] Android storage analysis:", { + hasRestrictions, + androidVersion, + fileName, + timestamp: new Date().toISOString(), + }); + + if (hasRestrictions) { + // For Android 10+ with storage restrictions, try app-specific external storage first + try { + // Try to save to app's external storage directory + await this.ensureDirectoryExists("TimeSafari", Directory.ExternalStorage); + + const result = await Filesystem.writeFile({ + path: `TimeSafari/${fileName}`, + data: content, + directory: Directory.ExternalStorage, + encoding: Encoding.UTF8, + }); + + logger.log("[CapacitorPlatformService] File saved to app external storage (Android 10+):", { + fileName, + uri: result.uri, + androidVersion, + timestamp: new Date().toISOString(), + }); + + return result.uri; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.warn("[CapacitorPlatformService] Failed to save to app external storage, trying Downloads:", { + fileName, + error: errorMessage, + androidVersion, + timestamp: new Date().toISOString(), + }); + } + } + + // Try Downloads directory (works on older Android versions) try { - // Attempt to save to Downloads directory (most accessible) - const downloadsPath = `Download/TimeSafari/${fileName}`; + await this.ensureDirectoryExists("Download/TimeSafari", Directory.ExternalStorage); const result = await Filesystem.writeFile({ - path: downloadsPath, + path: `Download/TimeSafari/${fileName}`, data: content, - directory: Directory.ExternalStorage, // External storage (persists between installations) + directory: Directory.ExternalStorage, encoding: Encoding.UTF8, - recursive: true, }); - logger.log("[CapacitorPlatformService] File saved to Android Downloads (persistent):", { - uri: result.uri, + logger.log("[CapacitorPlatformService] File saved to Downloads (Android):", { fileName, - downloadsPath, - note: "File persists between app installations and is accessible via file managers", + uri: result.uri, + androidVersion, timestamp: new Date().toISOString(), }); return result.uri; - } catch (downloadsError) { - logger.warn("[CapacitorPlatformService] Could not save to Downloads, using app external storage:", downloadsError); - - // Fallback: Save to app's external storage directory - const appStoragePath = `TimeSafari/${fileName}`; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.warn("[CapacitorPlatformService] Could not save to Downloads, using app external storage:", { + fileName, + error: errorMessage, + androidVersion, + timestamp: new Date().toISOString(), + }); + // Final fallback: app's external storage without subdirectory const result = await Filesystem.writeFile({ - path: appStoragePath, + path: fileName, data: content, - directory: Directory.ExternalStorage, // External storage (persists between installations) + directory: Directory.ExternalStorage, encoding: Encoding.UTF8, - recursive: true, }); - logger.log("[CapacitorPlatformService] File saved to Android app external storage (persistent):", { - uri: result.uri, + logger.log("[CapacitorPlatformService] File saved to external storage (fallback):", { fileName, - appStoragePath, - note: "File persists between app installations and is accessible via file managers", + uri: result.uri, + androidVersion, timestamp: new Date().toISOString(), }); @@ -765,8 +927,30 @@ export class CapacitorPlatformService implements PlatformService { } } } catch (error) { - logger.error("[CapacitorPlatformService] Save to persistent storage failed:", error); - throw new Error(`Failed to save to persistent storage: ${error}`); + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error("[CapacitorPlatformService] Save to persistent storage failed:", { + fileName, + error: errorMessage, + timestamp: new Date().toISOString(), + }); + + // Final fallback: app's data directory (not user-accessible but guaranteed to work) + logger.warn("[CapacitorPlatformService] Attempting final fallback to app data directory"); + + const result = await Filesystem.writeFile({ + path: fileName, + data: content, + directory: Directory.Data, + encoding: Encoding.UTF8, + }); + + logger.log("[CapacitorPlatformService] File saved to app data directory (fallback):", { + fileName, + uri: result.uri, + timestamp: new Date().toISOString(), + }); + + return result.uri; } } @@ -823,6 +1007,7 @@ export class CapacitorPlatformService implements PlatformService { } else { // Android: List files from persistent storage locations const allFiles: Array<{name: string, uri: string, size?: number}> = []; + // Try to list files from Downloads/TimeSafari directory try { const downloadsResult = await Filesystem.readdir({ @@ -840,8 +1025,14 @@ export class CapacitorPlatformService implements PlatformService { })); allFiles.push(...downloadFiles); } catch (downloadsError) { - logger.warn("[CapacitorPlatformService] Could not read Downloads/TimeSafari directory:", downloadsError); + const err = downloadsError as Error; + if (err.message.includes("User denied storage permission")) { + logger.warn("[CapacitorPlatformService] Storage permission denied for Downloads/TimeSafari, skipping"); + } else { + logger.warn("[CapacitorPlatformService] Could not read Downloads/TimeSafari directory:", downloadsError); + } } + // Try to list files from app's external storage directory try { const appStorageResult = await Filesystem.readdir({ @@ -859,8 +1050,14 @@ export class CapacitorPlatformService implements PlatformService { })); allFiles.push(...appStorageFiles); } catch (appStorageError) { - logger.warn("[CapacitorPlatformService] Could not read app external storage directory:", appStorageError); + const err = appStorageError as Error; + if (err.message.includes("User denied storage permission")) { + logger.warn("[CapacitorPlatformService] Storage permission denied for TimeSafari, skipping"); + } else { + logger.warn("[CapacitorPlatformService] Could not read app external storage directory:", appStorageError); + } } + // Remove duplicates based on filename const uniqueFiles = allFiles.filter((file, index, self) => index === self.findIndex(f => f.name === file.name) @@ -873,6 +1070,11 @@ export class CapacitorPlatformService implements PlatformService { return uniqueFiles; } } catch (error) { + const err = error as Error; + if (err.message.includes("User denied storage permission")) { + logger.warn("[CapacitorPlatformService] Storage permission denied, returning empty file list"); + return []; + } logger.error("[CapacitorPlatformService] Failed to list user accessible files:", error); return []; } @@ -1294,6 +1496,9 @@ export class CapacitorPlatformService implements PlatformService { // Try to save to the user-selected directory try { + // Ensure the selected directory exists before writing + await this.ensureDirectoryExists(directoryResult.path, Directory.ExternalStorage); + const result = await Filesystem.writeFile({ path: fullPath, data: content, @@ -1817,7 +2022,12 @@ export class CapacitorPlatformService implements PlatformService { })); allFiles.push(...appStorageFiles); } catch (error) { - logger.warn("[CapacitorPlatformService] Could not read TimeSafari external storage:", error); + const err = error as Error; + if (err.message.includes("User denied storage permission")) { + logger.warn("[CapacitorPlatformService] Storage permission denied for TimeSafari, skipping"); + } else { + logger.warn("[CapacitorPlatformService] Could not read TimeSafari external storage:", error); + } } // 2. Common user-chosen locations (if accessible) with recursive search @@ -1903,7 +2113,12 @@ export class CapacitorPlatformService implements PlatformService { } } } catch (subDirError) { - logger.warn(`[CapacitorPlatformService] Could not read subdirectory ${path}/${fileName}:`, subDirError); + const subDirErr = subDirError as Error; + if (subDirErr.message.includes("User denied storage permission")) { + logger.warn(`[CapacitorPlatformService] Storage permission denied for subdirectory ${path}/${fileName}, skipping`); + } else { + logger.warn(`[CapacitorPlatformService] Could not read subdirectory ${path}/${fileName}:`, subDirError); + } } } else { // Check if file matches backup criteria @@ -1932,9 +2147,13 @@ export class CapacitorPlatformService implements PlatformService { }); allFiles.push(...relevantFiles); } - } catch (error) { - // Silently skip inaccessible directories - logger.debug(`[CapacitorPlatformService] Could not access ${path}:`, error); + } catch (pathError) { + const pathErr = pathError as Error; + if (pathErr.message.includes("User denied storage permission")) { + logger.warn(`[CapacitorPlatformService] Storage permission denied for ${path}, skipping`); + } else { + logger.warn(`[CapacitorPlatformService] Could not read ${path}:`, pathError); + } } } } @@ -2277,8 +2496,113 @@ export class CapacitorPlatformService implements PlatformService { return entries; } catch (error) { + const err = error as Error; + if (err.message.includes("User denied storage permission")) { + logger.warn("[CapacitorPlatformService] Storage permission denied for directory listing, returning empty array:", { path, error: err.message }); + return []; + } logger.error("[CapacitorPlatformService] Failed to list directory:", { path, error }); return []; } } + + /** + * Provides user guidance when storage permissions are denied + * @returns Promise resolving to guidance message + */ + async getStoragePermissionGuidance(): Promise { + try { + if (this.getCapabilities().isIOS) { + return "On iOS, files are saved to the Documents folder and can be accessed via the Files app. No additional permissions are required."; + } else { + // For Android, provide guidance on how to grant permissions + return "To access your backup files, please grant storage permissions:\n\n" + + "1. Go to your device Settings\n" + + "2. Find 'Apps' or 'Application Manager'\n" + + "3. Find 'TimeSafari' in the list\n" + + "4. Tap 'Permissions'\n" + + "5. Enable 'Storage' permission\n" + + "6. Return to the app and try again\n\n" + + "Alternatively, you can save files to your app's private storage which doesn't require external storage permissions."; + } + } catch (error) { + return "Unable to provide permission guidance. Please check your device settings for app permissions."; + } + } + + /** + * Tests directory creation functionality to ensure the ensureDirectoryExists method works correctly. + * @returns Promise resolving to a test result message + */ + async testDirectoryCreation(): Promise { + try { + logger.log("[CapacitorPlatformService] Testing directory creation functionality"); + + if (this.getCapabilities().isIOS) { + return "✅ Directory creation test not needed on iOS - using Documents directory"; + } + + // Test creating the Downloads/TimeSafari directory + await this.ensureDirectoryExists("Download/TimeSafari", Directory.ExternalStorage); + + // Test creating the TimeSafari directory + await this.ensureDirectoryExists("TimeSafari", Directory.ExternalStorage); + + // Test creating a nested directory structure + await this.ensureDirectoryExists("Download/TimeSafari/Test", Directory.ExternalStorage); + + logger.log("[CapacitorPlatformService] Directory creation tests completed successfully"); + + return "✅ Directory creation tests passed successfully"; + } catch (error) { + const err = error as Error; + logger.error("[CapacitorPlatformService] Directory creation test failed:", error); + return `❌ Directory creation test failed: ${err.message}`; + } + } + + /** + * Detects the Android version to handle storage restrictions appropriately + * @returns Promise resolving to Android version number or null if not Android + */ + private async getAndroidVersion(): Promise { + try { + if (!this.getCapabilities().isIOS) { + // For Android, we can try to detect version from user agent + // This is a fallback since we don't have the Device plugin + const userAgent = navigator.userAgent; + const androidMatch = userAgent.match(/Android\s+(\d+)/); + if (androidMatch) { + const version = parseInt(androidMatch[1], 10); + logger.log("[CapacitorPlatformService] Android version detected from user agent:", { + version, + userAgent: userAgent.substring(0, 100) + "...", + timestamp: new Date().toISOString(), + }); + return version; + } + + // If we can't detect from user agent, assume Android 10+ for safety + logger.log("[CapacitorPlatformService] Could not detect Android version, assuming 10+ for safety"); + return 10; + } + return null; + } catch (error) { + logger.warn("[CapacitorPlatformService] Could not detect Android version:", { + error: error instanceof Error ? error.message : String(error), + timestamp: new Date().toISOString(), + }); + return null; + } + } + + /** + * Checks if the current Android version has storage restrictions + * @returns Promise resolving to true if storage restrictions apply + */ + private async hasStorageRestrictions(): Promise { + const androidVersion = await this.getAndroidVersion(); + // Android 10 (API 29) and above have stricter storage restrictions + return androidVersion !== null && androidVersion >= 29; + } }