[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-96256":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":9,"language":10,"languages":9,"totalLinesOfCode":9,"stars":11,"forks":12,"watchers":13,"openIssues":14,"contributorsCount":15,"subscribersCount":15,"size":15,"stars1d":15,"stars7d":15,"stars30d":15,"stars90d":15,"forks30d":15,"starsTrendScore":15,"compositeScore":16,"rankGlobal":9,"rankLanguage":9,"license":17,"archived":18,"fork":18,"defaultBranch":19,"hasWiki":20,"hasPages":20,"topics":21,"createdAt":9,"pushedAt":9,"updatedAt":22,"readmeContent":23,"aiSummary":24,"trendingCount":15,"starSnapshotCount":15,"syncStatus":25,"lastSyncTime":26,"discoverSource":27},96256,"PhotosBackup","g8row\u002FPhotosBackup","g8row","PhotosBackup iOS app",null,"Swift",106,12,1,7,0,43.34,"MIT License",false,"main",true,[],"2026-09-20 04:01:32","# Photos Backup for iOS\n\n\u003Cp align=\"center\">\n  \u003Cimg src=\"App\u002FResources\u002FAssets.xcassets\u002FAppIcon.appiconset\u002FAppIcon-1024.png\" width=\"160\" alt=\"Photos Backup app icon\">\n\u003C\u002Fp>\n\nAn experimental, fully on-device iPhone app for backing up selected photos,\nvideos, and albums to Google Photos. It is a single SwiftUI app that completes\nGoogle account setup in an in-app web view, so everything happens on the phone\nwithout a desktop companion or hosted service.\n\n> [!WARNING]\n> This project uses Google's private, undocumented Photos endpoints and an\n> Android-style authentication flow. It is not affiliated with or endorsed by\n> Google, and the integration may stop working without notice. Treat it as\n> experimental software and use it at your own risk.\n\n## What it can do\n\n- Connect a Google account through Google's EmbeddedSetup flow, in an in-app web view.\n- Capture the single-use `oauth_token` in-process from the web view's cookie store.\n- Exchange the token for a Google Photos credential entirely on the device.\n- Select albums from the local Photos library.\n- Queue individual photos, videos, or all items in selected albums.\n- Show hashing, duplicate-check, upload, and finalization progress per item.\n- Avoid re-uploading media already present in Google Photos.\n- Retry transient failures, cancel work, and resume after reconnecting.\n- Show why an upload failed in Google's own words, copyable from the row and\n  from Diagnostics, and stop the queue when the Google account is out of space.\n- Restore pending album uploads after an app restart and remember completed\n  library assets per Google account.\n- Show per-album backup progress, and re-upload assets edited after backup.\n- Upload in original quality or request Google's Storage Saver processing.\n- Choose how many uploads run at once, from 1 to 10.\n- Enforce Wi-Fi-only or Wi-Fi-and-cellular policy at queue and request level,\n  cancelling in-flight background transfers when the allowed transport is lost.\n- Request recurring iOS background-processing windows for selected-album backup.\n- Expose a **Back Up Photos** Shortcuts action on iOS 16+ for charger,\n  time-of-day, Wi-Fi, and other personal automations. It runs even with\n  Automatic Backup off, so a schedule of your choosing can replace it.\n- Keep file PUTs running in an iOS-owned background `URLSession`, then commit\n  completed receipts when iOS relaunches the app.\n- Track PhotoKit persistent changes on iOS 16+ so backdated imports are found.\n- Keep going when one item keeps closing the app: an item the app stopped on\n  twice while preparing it is skipped, and can be retried, instead of stopping\n  every relaunch.\n- Create a privacy-safe diagnostic report with a plain-language summary, the\n  recent runs and the conditions iOS ran them under, crash and termination data\n  from iOS, and a timeline of what the app decided and why.\n- Store usable long-lived credentials in the iOS Keychain when signing permits.\n\n## Current status\n\nThe complete authentication path has been proven on an iOS 17 device and\nsimulator: the in-app web view receives the `oauth_token`, the app reads it from\nthe web view's own cookie store, exchanges it for an unbound master token and\nPhotos credential, and an authenticated `photosdata-pa` request succeeds.\n\nThe Xcode project, app target, and scheme are named `PhotosBackup`; the\nuser-facing app is named **Photos Backup**.\n\nLatest release: **0.3.6** ([releases](https:\u002F\u002Fgithub.com\u002Fg8row\u002FPhotosBackup\u002Freleases)).\n161 tests run on an iPhone simulator: 158 pass. The 2 opt-in live tests and\nthe Keychain round trip, which needs a signed build, are skipped.\n\n### App identity (since 0.0.2)\n\n| Piece | Value |\n| --- | --- |\n| App bundle ID | `com.g8row.photosbackup` |\n| Background task | `com.g8row.photosbackup.background-backup` |\n| Background upload session | `com.g8row.photosbackup.background-upload` |\n\n> [!IMPORTANT]\n> The bundle ID and Keychain service changed in 0.0.2. After updating from an\n> older build, reconnect the Google account once, then force-quit and reopen\n> to confirm it stays connected.\n\n## Reporting a problem\n\nOpen **Settings → Support → Create Diagnostic Report**, tap **Generate\nReport**, then **Share or Save Report**. Attach the text file to a\n[GitHub issue](https:\u002F\u002Fgithub.com\u002Fg8row\u002FPhotosBackup\u002Fissues). The report\nopens with a short **What stands out** list that often explains the problem on\nits own. It leaves out credentials, account addresses, photo identifiers,\nfilenames, media, and request URLs.\n\n**Diagnostics → Event Timeline** shows the same timeline inside the app. Crash\ndetails from iOS are included only when Share With App Developers is on\n(Settings → Privacy & Security → Analytics & Improvements); they arrive a day\nor so after the crash.\n\n## Requirements\n\n- macOS with Xcode 16.4 and an installed iOS Simulator runtime\n- [XcodeGen](https:\u002F\u002Fgithub.com\u002Fyonaskolb\u002FXcodeGen) 2.40 or newer\n- iOS 15.0 or newer\n- A Google account for the live connection flow\n- For a physical device: an Apple signing identity, or a sideloading tool such\n  as SideStore or AltStore\n\nInstall XcodeGen with Homebrew if needed:\n\n```sh\nbrew install xcodegen\n```\n\n## Build and run\n\nGenerate the Xcode project:\n\n```sh\nxcodegen generate\nopen PhotosBackup.xcodeproj\n```\n\nSelect the `PhotosBackup` scheme and an iPhone simulator in Xcode, then run the\napp. A command-line simulator build also works:\n\n```sh\nxcodebuild \\\n  -project PhotosBackup.xcodeproj \\\n  -scheme PhotosBackup \\\n  -destination 'generic\u002Fplatform=iOS Simulator' \\\n  CODE_SIGNING_ALLOWED=NO \\\n  build\n```\n\nFor a signed device build, set `DEVELOPMENT_TEAM` in `project.yml`, regenerate\nthe project, and let Xcode manage signing.\n\n### Test background execution\n\nDebug builds expose **Settings → Diagnostics → Simulate Background Run**. This\nruns the same scan\u002Fenqueue\u002Fwait path immediately and is the fastest normal test\nloop.\n\nTo exercise the actual `BGProcessingTask` launch handler on a connected device,\nrun the app from Xcode, background it, pause the debugger, and enter this in the\nLLDB console:\n\n```text\ne -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@\"com.g8row.photosbackup.background-backup\"]\n```\n\nThe Diagnostics screen also has a button that copies this command.\n\n### Build an unsigned IPA\n\nThe repository includes a packaging script for SideStore\u002FAltStore-style\nsideloading:\n\n```sh\n.\u002FScripts\u002Fmake-ipa.sh\n```\n\nThe script defaults to `DEVELOPER_DIR=\u002FApplications\u002FXcode-16.4.0.app\u002FContents\u002FDeveloper`.\nOverride it only if Xcode lives elsewhere:\n\n```sh\nDEVELOPER_DIR=\u002Fpath\u002Fto\u002FXcode.app\u002FContents\u002FDeveloper .\u002FScripts\u002Fmake-ipa.sh\n```\n\nThe unsigned package is written to `build\u002FPhotosBackup.ipa`. The sideloading\ntool re-signs it with the Apple ID configured on the device.\n\n## Install via SideStore\n\nPrebuilt unsigned IPAs are attached to each\n[GitHub release](https:\u002F\u002Fgithub.com\u002Fg8row\u002FPhotosBackup\u002Freleases).\n\nTo get new versions automatically, add this source in SideStore, AltStore, or\nFeather. It is regenerated from every release:\n\n```text\nhttps:\u002F\u002Fg8row.github.io\u002FPhotosBackup\u002Fapps.json\n```\n\n> [!TIP]\n> On your iPhone (with SideStore or AltStore installed), one-tap install of\n> the latest release:\n>\n> - **[Install Photos Backup](https:\u002F\u002Fg8row.github.io\u002FPhotosBackup\u002Finstall.html)** —\n>   open on the iPhone and tap Install with SideStore \u002F AltStore.\n>\n> GitHub strips custom `sidestore:\u002F\u002F` \u002F `altstore:\u002F\u002F` URL schemes in markdown,\n> so the buttons live on that page instead of directly in this README. It\n> installs\n> `https:\u002F\u002Fgithub.com\u002Fg8row\u002FPhotosBackup\u002Freleases\u002Flatest\u002Fdownload\u002FPhotosBackup.ipa`.\n\n- AirDrop `PhotosBackup.ipa` to the iPhone and save it in Files.\n- Turn on LocalDevVPN.\n- In SideStore, tap +, choose `PhotosBackup.ipa`, and install it.\n- After updating across the 0.0.2 bundle-ID change, reconnect the Google\n  account once.\n\n## Connect a Google account\n\n1. Install and launch Photos Backup.\n2. In onboarding (or Settings → Connect Account), tap **Connect Google Account**.\n3. Sign in and accept Google's consent prompt in the in-app window. The page may\n   remain on a spinner afterward; that is expected — the app captures the token\n   and closes the window on its own.\n4. Grant the desired Photos access and select albums.\n\nThe captured `oauth_token` is single-use and is read once from the web view's\ncookie store, then the web session is discarded.\n\n## Authentication and credential handling\n\nThe normal flow is:\n\n```text\nIn-app EmbeddedSetup web view\n        │  oauth_token (read from WKHTTPCookieStore)\n        ▼\nAndroid master token → Photos access token → private Photos API\n```\n\n- The exchange runs locally; there is no companion backend.\n- Credentials are stored as a single Keychain item using\n  `AfterFirstUnlockThisDeviceOnly` when Keychain access is available.\n- Exported Photos-library items are staged in protected Application Support,\n  retained while a background transfer owns them, and removed afterward.\n- The `oauth_token` is read in-process from the app's own non-persistent web\n  view cookie store; it never leaves the app via an extension, App Group, or\n  custom URL scheme.\n- Bound\u002Fencrypted Google tokens are rejected because token binding is not\n  implemented.\n\n## Known limitations\n\n- Google can change or disable the private authentication and Photos endpoints.\n- Live Photos currently upload only their still image; the motion component is\n  ignored.\n- Background album backup is opportunistic: iOS decides when each processing\n  request runs and may delay it based on usage, battery, and system policy.\n- Shortcuts can create extra backup opportunities on iOS 16+, but iOS gives\n  each run about 30 seconds. The action queues durable work and gives prepared\n  file transfers to the background URL session; it is not a periodic guarantee.\n- Background scans enqueue bounded batches of 250. The limit bounds memory, not\n  how much a window uploads: the queue is durable, so whatever a window cannot\n  finish waits for the next one. Foreground scans and the manual Back Up Now and\n  Re-check Backups buttons queue the whole selection at once, so the count they\n  report is the full run and the queue's concurrency setting decides how much of\n  it moves at a time.\n  iOS 16+ background scans use a persistent PhotoKit change token; iOS 15 and\n  expired-token recovery use a correctness-first current-library scan. The token\n  advances once a scan's sources have all been handed to the queue, so a\n  saturated queue stops re-enumerating the library on every window.\n- Export, hashing, duplicate lookup, and upload initialization still need an\n  execution window. Once initialized, the file PUT continues under iOS even if\n  the processing window expires; the app persists the receipt before commit.\n- Cloud-only PhotoKit resources are deferred during short background processing\n  windows and resume with network access when the app is foregrounded.\n- Unsigned simulator builds cannot persist the credential in the Keychain.\n  Free personal-team builds normally expire after seven days and must be\n  refreshed.\n- Google accounts that receive a bound\u002Fencrypted master token are unsupported.\n- This is not an App Store-ready release.\n\n## Tests\n\nRun the offline unit test suite against any installed simulator:\n\n```sh\nxcodebuild \\\n  -project PhotosBackup.xcodeproj \\\n  -scheme PhotosBackup \\\n  -destination 'platform=iOS Simulator,name=\u003Cyour simulator>' \\\n  CODE_SIGNING_ALLOWED=NO \\\n  test\n```\n\nList available simulator names with:\n\n```sh\nxcrun simctl list devices available\n```\n\nLive tests are opt-in because they contact Google. The full exchange test also\nrequires a fresh, single-use `oauth_token`:\n\n```sh\nTEST_RUNNER_GPMC_LIVE=1 \\\nxcodebuild ... test \\\n  -only-testing:PhotosBackupTests\u002FLiveExchangeTests\u002FtestInvalidTokenIsRejectedByGoogleNotByUs\n\nTEST_RUNNER_GPMC_LIVE=1 \\\nTEST_RUNNER_GPMC_OAUTH_TOKEN=oauth_XXXX \\\nxcodebuild ... test \\\n  -only-testing:PhotosBackupTests\u002FLiveExchangeTests\u002FtestFullExchangeWithRealToken\n```\n\nNever commit tokens or captured account credentials.\n\n## Repository layout\n\n```text\nApp\u002FSources\u002F                  SwiftUI app, onboarding, account, and upload queue\nApp\u002FSources\u002FAutomaticBackupCoordinator.swift  BGProcessingTask scheduling\nApp\u002FSources\u002FBackgroundUploadTransport.swift   Relaunch-safe file PUT transport\nApp\u002FSources\u002FPhotoLibraryChangeTracker.swift   Persistent PhotoKit scan token\nApp\u002FSources\u002FNetworkPolicy.swift               Wi-Fi-only \u002F cellular enforcement\nApp\u002FSources\u002FUploadQueuePersistence.swift      Durable account-scoped queue\nApp\u002FResources\u002F                Info.plist and app icon assets\nApp\u002FSources\u002FAccountConnectWebView.swift       In-app EmbeddedSetup web view\nGPMC\u002FCore\u002F                    Photos protocol client and protobuf helpers\nTests\u002FPhotosBackupTests\u002F      Offline unit tests and gated live tests\nScripts\u002Fmake-ipa.sh           Unsigned IPA packaging\ndocs\u002F                         Feasibility log and authentication ADR\nproject.yml                   XcodeGen project definition\n```\n\nFor implementation history and protocol details, see:\n\n- [`docs\u002FADR-001-auth-route.md`](docs\u002FADR-001-auth-route.md)\n- [`docs\u002Ffeasibility-probe.md`](docs\u002Ffeasibility-probe.md)\n\n## Acknowledgements\n\nThe protocol work is based on [GPMC by xob0t](https:\u002F\u002Fgithub.com\u002Fxob0t\u002Fgpmc),\nand the browser authentication route is based on gotohp. The pinned upstream\nrevisions and design rationale are recorded in the authentication ADR.\n\n## License\n\nThis project is available under the [MIT License](LICENSE).\n","PhotosBackup 是一款运行在 iOS 设备上的本地化照片与视频备份工具，支持将用户精选的相册、照片及视频直接上传至 Google Photos。其核心功能包括：基于 WebView 的端内 Google 账户认证（使用非公开 API）、端侧 OAuth 流程与凭证管理、智能去重（通过哈希比对云端已存在媒体）、断点续传与后台上传、Wi-Fi\u002F蜂窝网络策略控制、Shortcuts 自动化集成，以及隐私优先的诊断报告生成。所有操作均在设备本地完成，无需服务器中转或桌面端配合。适用于希望自主控制备份时机、避免依赖 iCloud 或系统自动备份、且信任 Google Photos 存储的 iOS 用户。",2,"2026-09-12 02:30:15","CREATED_QUERY"]