zakariaf/flutter-skills

data-export-and-restore

- Enforces user-facing data portability in an offline-first app — backup/restore (exact, machine round-trippable) kept strictly separate from export/report (human, lossy, never a restore source); a versioned envelope carrying formatVersion, schemaVersion, a…

소스 보기

사용 방법

  1. 설치 명령을 복사하고 자신의 에이전트 환경에 설치하기 전에 소스 패키지를 확인하세요.
  2. 필요한 도구, 인증 정보, 지원 환경은 원본 문서에서 확인하세요.
  3. 입력과 원하는 산출물을 설명하고 출처와 결과를 대조하세요.
npx skills add https://github.com/zakariaf/flutter-skills --skill data-export-and-restore

시작 전 확인

  • 설치에는 모델 구독, API 한도, 파일 접근 권한이 포함되지 않습니다.
  • 별점 수와 설치 수는 활동 지표이며 독립적인 품질·보안 검증이 아닙니다.
원본 Skill 문서

원본 저장소의 제목, 예시, 코드, 표, 링크, 이미지를 유지해 표시합니다.

Data export and restore

In an app with no server, the export file is the only copy of a user's data that can outlive the phone, and restore is the only recovery path they can invoke themselves. Both are dangerous in opposite directions: a lossy export silently becomes someone's backup, and a half-applied restore destroys the very database it was meant to protect. This skill governs the portable artifacts; the on-device VACUUM INTO snapshot and WAL rules belong to persistence-drift.

Depth lives in references/formats-and-encoding.md (CSV/JSON/PDF mechanics) and references/restore-and-merge-policy.md (validation ladder, merge strategies).

Non-negotiable rules

  1. Backup and export are two different features — never conflate them. Backup is

exact, machine-read, round-trippable, and the only thing restore accepts. Export (CSV for a spreadsheet, PDF for a human) is an interop artifact, lossy by design, and must be refused as a restore source. WHY: the moment a CSV is restorable, a format built for readability becomes the file someone's records depend on.

  1. Every backup carries an envelope, and restore reads it first. formatVersion,

schemaVersion, appVersion, exportedAtUtc, a checksum over the payload, then the payload. Restore verifies the checksum, refuses schemaVersion greater than the app's with a plain message, and migrates an older payload through the same forward-only path as the database (run-migration). WHY: an unversioned file cannot be refused, only misread.

  1. Restore is all-or-nothing and never edits the live database in place. Import

into a fresh staging database, validate everything, close every handle, then publish by atomic rename with the previous file kept as a rollback until the new one opens cleanly. WHY: a partial restore is worse than a failed one — it leaves a state the user cannot describe and you cannot reproduce.

  1. Machine formats carry canonical values only. Integer minor units, SI integers,

ISO-8601 UTC instants, stable ids, untranslated enum codes. Never a localized numeral, a formatted date, a currency symbol, or a translated label in a file that will be parsed. Localize only in human-facing exports, which are never re-imported. WHY: ١٢٫٥ and 12.5 and 12,5 are the same quantity and three different parses.

  1. Identity is content-assigned, never a rowid. Every exportable row carries a

stable id (UUID/ULID) minted at creation. WHY: re-importing the same file must be idempotent — with autoincrement ids it duplicates every record instead.

  1. State the merge policy and show it before it runs. Replace-all (wipe then

import) and merge-by-id (last-write-wins on a stored updatedAtUtc) are different promises; the confirmation names which one is about to happen and what will be lost. WHY: "Restore" reads as "add my data back" to a user who is about to lose a week.

  1. Stream to a temp file, publish by rename. Build the artifact through an IOSink

into the app's own temp directory and rename it into place only when the last byte is flushed. WHY: a 100k-row export assembled in a String OOMs a cheap phone, and a half-written file must never be shareable.

  1. **Nothing leaves the sandbox without an explicit user action, through an injected

Gateway.** No auto-upload, no background sync, no "helpful" cloud copy. The share sheet and file-save picker sit behind a ShareGateway/FileExportGateway (service-boundary-and-native), faked in tests.

  1. CSV is escaped twice: RFC 4180 and against formula injection.* Quote any field

containing the delimiter, a quote, CR or LF, doubling embedded quotes; and neutralize a leading =, +, -, @, tab or CR so a spreadsheet renders the text instead of executing it. WHY: an unescaped cell is a code-execution vector in the recipient's spreadsheet, and a lost row in yours.

  1. Export and restore return typed results, never throw at the UI.

Result<ExportArtifact, ExportFailure> / Result<RestoreReport, RestoreFailure> with a distinct failure per refusal reason (checksum, unsupported version, malformed payload, no space, permission denied). WHY: "Restore failed" with no reason is indistinguishable from data loss to the person reading it.

  1. A failed export leaves no artifact. Delete the temp file on every failure path

and never publish a partial one. WHY: a truncated file that looks like a backup is the failure mode that surfaces months later.

  1. Say what an export is. Exports are plaintext and unencrypted unless you built

encryption on purpose; the UI must say so at the moment of sharing, and the claim must match the code (see release-and-store-shipping for claim wording).

The envelope

dart
/// The only shape restore accepts. Version fields come FIRST so a truncated or
/// foreign file is rejected by the header rather than by a mid-parse exception.
({
  int formatVersion,        // this envelope's own shape — bump independently of the DB
  int schemaVersion,        // the DB schema the payload was written from
  String appVersion,        // provenance for support, never a compatibility check
  DateTime exportedAtUtc,   // UTC, from the injected Clock
  String payloadSha256,     // checksum over the payload bytes, verified before parsing
  Map<String, Object?> payload,
});

formatVersion and schemaVersion are separate on purpose: the envelope can gain a field without a database migration, and the database can migrate without changing the file shape. A single "version" number conflates them and forces a false choice on the next change. Complete, runnable shape: examples/backup_envelope.dart.

Restore: validate, stage, swap

dart
Future<Result<RestoreReport, RestoreFailure>> restore(File picked) async {
  // 1. Header and integrity BEFORE anything is parsed or opened.
  final envelope = readEnvelope(picked);
  switch (envelope) {
    case Err(:final failure): return Err(failure);           // malformed / bad checksum
    case Ok(:final value) when value.schemaVersion > kSchemaVersion:
      return const Err(RestoreFailure.newerThanApp());       // refuse, never guess
    case Ok():
      break;
  }

  // 2. Import into a STAGING database — the live one is never touched yet.
  final staging = await openStagingDatabase();
  final imported = await staging.transaction(() => _importAll(envelope.value.payload));
  if (imported case Err(:final failure)) {
    await staging.close();
    await deleteStagingFiles();                              // user's data still intact
    return Err(failure);
  }

  // 3. Migrate the staged copy forward through the SAME path as the app database.
  //    4. Close every handle, then publish by rename, keeping the old file until the
  //       new one opens cleanly. See references/restore-and-merge-policy.md.
  return publishStagedDatabase(staging);
}

The staging file, the migration path, and the rename ordering are the whole correctness argument — a restore that writes directly into the open app database has no failure mode that leaves the user where they started.

Choosing a format

NeedFormatRestorable?
Full-fidelity backup the app reads backEnvelope + JSON payload (or a copied DB file)Yes — the only one
Rows for a spreadsheet or another toolCSV, RFC 4180, canonical valuesNo
Something a human reads, prints, or filesPDF, fully localized and formattedNo

Human-facing exports localize at the edge like any other rendering (value-objects-money-and-units, i18n-rtl-l10n); machine formats never do. Copying the live database file directly is a valid backup only through the WAL-safe primitive in persistence-drift — never File.copy on an open database.

Tests that must exist

  • Round trip on a hostile fixture. Export → import into an empty database → export

again → byte-identical. The fixture carries apostrophes, quotes, commas, embedded newlines, emoji, RTL text with bidi marks, whitespace-only strings, the largest supported number, a DST-ambiguous local instant, and a null-vs-empty pair. 'test1' survives everything and proves nothing.

  • Idempotent re-import. Importing the same file twice produces the same state under

merge-by-id — not duplicates.

  • Every refusal is a test: truncated file, flipped checksum byte, schemaVersion

one higher than the app, an older schemaVersion that must migrate, empty payload, and a file that is not this format at all. Each asserts the typed failure and that the live database is byte-unchanged.

  • Failure leaves no artifact. Force a mid-write failure; assert no publishable file

exists in the export directory.

  • CSV escaping. A cell containing =cmd(), one containing a,b"c\nd, and an RTL

cell each survive a write→read round trip through a real parser.

Anti-patterns

  • CSV as the backup format — no envelope, no types, no versioning; it will be

someone's only copy.

  • An unversioned export file — the first schema change makes every existing file

either unreadable or, worse, silently misread.

  • Restoring straight into the live database — no rollback exists; a mid-import

failure leaves a state nobody can describe.

  • Autoincrement rowids as export identity — re-import duplicates everything.
  • Formatting numbers or dates in a machine format — locale-dependent output that

round-trips only on the machine that wrote it.

  • Building the export in a `String`/`StringBuffer` — fine for 100 rows, an OOM

crash at 100k, on the cheapest device you support.

  • Writing directly to the shareable path — a crash mid-write publishes a truncated

file that still looks like a backup.

  • Auto-uploading or auto-backing-up "for safety" — it converts an offline app into

a data processor, changing the store privacy declaration and breaking the claim.

  • Trusting a picked file's extension or name — validate the envelope, not the path.
  • A generic "Import failed" message — the user cannot tell a wrong file from lost

data; name the reason from the typed failure.

  • Silently merging when the user expected replace (or vice versa) — state the

policy in the confirmation, in the same words as the code.

Definition of done

  • [ ] Backup and export are separate paths; export artifacts are refused by restore.
  • [ ] Every backup carries formatVersion, schemaVersion, appVersion,

exportedAtUtc, and a payload checksum, in that header-first order.

  • [ ] Restore verifies the checksum, refuses a newer schemaVersion, and migrates an

older payload through the same forward-only path as the database.

  • [ ] Restore imports into a staging database and publishes by rename; a failure leaves

the live database byte-unchanged.

  • [ ] Machine formats carry canonical values and stable content ids only.
  • [ ] The merge-vs-replace policy is explicit in code and named in the confirmation UI.
  • [ ] Writes stream to a temp file and are published by atomic rename; failures delete

the temp file and publish nothing.

  • [ ] Share/save goes through an injected Gateway on an explicit user action; nothing

uploads automatically.

  • [ ] CSV output is RFC 4180 quoted and formula-injection escaped.
  • [ ] Export/restore return typed Results with one failure per refusal reason.
  • [ ] Round-trip, idempotent-re-import, every-refusal, no-partial-artifact, and CSV

escaping tests pass on a hostile fixture.

Related skills

  • persistence-drift — the WAL-safe on-device snapshot primitive and the DAO layer the

importer writes through; never File.copy a live database.

  • run-migration — the forward-only path an older payload is migrated through.
  • error-handling-typed-results — the Result/Failure spine and the never-lose-data

guarantees this feature is the user-facing end of.

  • value-objects-money-and-units — canonical storage (minor units, SI, UTC) that makes

a machine format round-trip.

  • i18n-rtl-l10n — localize-at-render, and why localized numerals never enter a file.
  • service-boundary-and-native — the ShareGateway/file-picker seam and its fake.
  • release-and-store-shipping — the privacy claims an export feature must not break.
  • testing-strategy — hostile fixtures, property/round-trip tests, injected Clock.

References

  • RFC 4180 — Common Format and MIME Type for CSV Files: https://www.rfc-editor.org/rfc/rfc4180
  • Dart — dart:io IOSink, File.rename: https://api.dart.dev/stable/dart-io/File-class.html
  • path_provider — app directories: https://pub.dev/packages/path_provider
  • share_plus — share sheet (incl. sharePositionOrigin): https://pub.dev/packages/share_plus
  • file_selector — save/open dialogs: https://pub.dev/packages/file_selector
  • crypto — SHA-256 for the payload checksum: https://pub.dev/packages/crypto
  • OWASP — CSV Injection: https://owasp.org/www-community/attacks/CSV_Injection

관련 작업

같은 저장소의 Skills

더 많은 Skills

모든 Skills
zakariaf
커뮤니티

accessibility-as-code

Enforces accessibility as a correctness property authored into each widget — Semantics(button/label) or ExcludeSemantics on every node, a11y state read from MediaQuery not app state, never MediaQuery.withClampedTextScaling / textScaleFactor / FittedBox / TextOverflow.ellipsis to fit a label, non-color redundant channels (icon+label+shape+text) for every state, contrast against composited backgrounds (4.5:1 body / 3:1 large), 44px single-tap targets, OrdinalSortKey traversal, and honoring boldTex

설치 수
1
GitHub Stars
15
업데이트
9월 23일
zakariaf
커뮤니티

adaptive-layout

Enforces adapting layout by available CONSTRAINTS/size, never device or platform checks — LayoutBuilder + MediaQuery.sizeOf/paddingOf/viewInsetsOf (not .of) for narrow rebuilds, Material 3 window size classes (compact 1200) as the breakpoint vocabulary, navigation affordance chosen by width (NavigationBar → NavigationRail → NavigationDrawer), list-detail single-pane-vs-two-pane, readable max-width via ConstrainedBox, Flexible/Expanded/FractionallySizedBox over fixed widths, SafeArea + display cu

설치 수
1
GitHub Stars
15
업데이트
9월 23일
zakariaf
커뮤니티

ads-and-iap-monetization

- Enforces opt-in, rewarded-first monetization over the service seam — a rewarded view grants exactly one benefit and only on a genuine reward outcome (a sealed Rewarded/Dismissed/NoFill), earn loops capped per day against an injected Clock, the earn control preloaded and hidden unless an ad is loaded so a fresh account never dead-ends on "no ads available" (a Guideline 2.1 risk), interstitials only at a natural break under a count-and-elapsed cap owned by the service so callers stay dumb, no ba

설치 수
1
GitHub Stars
15
업데이트
9월 23일
zakariaf
커뮤니티

custom-canvas-and-gestures

Enforces CustomPainter/Canvas discipline — the View/Painter/Scene split with a dumb painter fed one immutable Scene value type, shouldRepaint as a single value compare kept strictly separate from the AnimationController-as-repaint animation path, one shared affine transform read by BOTH painter and hit-tester (toCanvas/toLogical exact inverses, never re-derive scale), geometry hit-testing (integer lattice or rasterized region-ID buffer, never Path.contains), zero-allocation paint(), gesture-as-p

설치 수
1
GitHub Stars
15
업데이트
9월 23일