Format danych

Wszystko, co SyncPocket App zapamiętuje, jest przechowywane lokalnie w wersjonowanym archiwum JSON zawierającym elementy i definicje Tablic oraz w folderze blobs na zawartość binarną. Ta strona jest referencją formatu.

Stabilność: poniższy format odzwierciedla aktualne źródło prawdy (Packages/MemoryKit). Do wydania 1.0 może ewoluować; aplikacja migruje stare dane automatycznie (już teraz migruje starszy format ClipStack).

Układ na dysku

~/Library/Application Support/MemoryApp/
├── items.json          # wersjonowany HistoryEnvelope (elementy + Tablice)
├── items.backup.json   # ostatni poprawny, atomowy snapshot
└── blobs/              # zawartość binarna wskazywana z items.json
    ├── <nazwa>.rtf    # zawartość rich-text
    └── <nazwa>.png    # obrazy

HistoryEnvelope

items.json jest wersjonowanym HistoryEnvelope. Wersja 2 zapisuje savedAt oraz jedno atomowe archive z dwiema tablicami: items i boards. Wspólny zapis zapobiega utrwaleniu członkostwa elementu bez odpowiadającej definicji Tablicy.

Schemat ClipItem

Każdy element archive.items to ClipItem — platformowo neutralna struktura Swift Codable (tylko Foundation + CryptoKit, współdzielona przez macOS i iOS):

PoleTypOpis
idUUID stringUnikalny identyfikator elementu, generowany w chwili przechwycenia.
typestringJedna z wartości ClipType: text, link, color, image, file.
datedateOstatni moment przechwycenia/promocji (standardowe kodowanie dat Swift Codable); steruje retencją.
createdAtdatePierwszy moment utworzenia. date jest odświeżane po ponownym przechwyceniu tej samej treści i jest znacznikiem używanym przez retencję.
plainTextstring · opcjonalneCzysto tekstowa treść dla elementów text, link i color.
rtfFilestring · opcjonalneNazwa pliku w blobs/ z zawartością RTF, gdy skopiowany tekst miał formatowanie.
imageFilestring · opcjonalneNazwa pliku w blobs/ z obrazem dla elementów image.
fileURLsstring[]Adresy URL plików dla elementów file (pliki skopiowane w Finderze). Puste dla pozostałych typów.
sourceAppNamestring · opcjonalneCzytelna nazwa aplikacji, z której skopiowano treść.
sourceBundleIDstring · opcjonalneIdentyfikator bundle aplikacji źródłowej (używany też przez listę wykluczeń).
pinnedbooleanTrzyma element na górze i chroni przed retencją czasową. Domyślnie false.
contentHashstringSHA-256 treści, małe litery hex — klucz deduplikacji.
boardIDsUUID string[]Identyfikatory Tablic opartych na członkostwie. Loved i Saved mają stałe ID; Pinned wynika z osobnego pola pinned. Duplikaty są usuwane z zachowaniem kolejności.
reminderDatedate · opcjonalneZaplanowane lokalne przypomnienie. Dopóki pole istnieje, chroni element przed retencją do chwili jawnego wyczyszczenia.
modifiedAtdateOstatnia zmiana stanu widocznego dla użytkownika. Konflikt między urządzeniami wygrywa nowsza wartość; starsze archiwa przyjmują tutaj date.
deletedAtdate · opcjonalneGdy istnieje, element znajduje się w Usuniętych. Jest kasowany bezpowrotnie 30 dni po tym czasie, chyba że wcześniej zostanie przywrócony.

Przykład

{
  "version": 2,
  "savedAt": 804108100.0,
  "archive": {
    "items": [{
      "id": "5B1E9C2A-8F1D-4A5E-9C77-2A1B3C4D5E6F",
      "type": "color",
      "date": 804108000.0,
      "createdAt": 804108000.0,
      "modifiedAt": 804108000.0,
      "plainText": "#FF6B6B",
      "fileURLs": [],
      "sourceAppName": "Figma",
      "sourceBundleID": "com.figma.Desktop",
      "pinned": false,
      "boardIDs": ["F0000000-0000-4000-8000-000000000001"],
      "reminderDate": 804799200.0,
      "contentHash": "a1b2c3…64 znaki hex…d4e5f6"
    }],
    "boards": [{
      "id": "F0000000-0000-4000-8000-000000000001",
      "name": "Loved",
      "kind": "loved",
      "createdAt": -978307200.0
    }]
  }
}

Schemat MemoryBoard

Każdy element archive.boards jest rekordem MemoryBoard:

PoleTypOpis
idUUID stringStabilny identyfikator Tablicy.
namestringNazwa Tablicy widoczna dla użytkownika.
kindstringloved, saved albo custom.
createdAtdateZnacznik utworzenia używany do stabilnej kolejności.

Pinned, Loved i Saved używają stałych ID Tablic (F000…000, F000…001 i F000…002) i są automatycznie przywracane w starszych archiwach. Członkostwo Pinned wynika z pinned, a isLoved i isSaved są wyliczane z boardIDs.

ClipType

WartośćNazwa wyświetlanaSF Symbol
textTekstdoc.text
linkLinklink
colorKolorpaintpalette
imageObrazphoto
filePlikifolder

Wykrywanie typu tekstu

Skopiowany tekst jest klasyfikowany przez ClipItem.detectTextType(_:) według tych reguł, w kolejności (wejście jest najpierw przycinane z białych znaków):

  1. Kolor — pasuje do ^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$, czyli #RGB, #RRGGBB lub #RRGGBBAA.
  2. Link — nie zawiera spacji ani nowych linii, parsuje się jako URL ze schematem http lub https i niepustym hostem.
  3. Tekst — cała reszta.

Hashowanie treści i deduplikacja

Każdy element ma contentHash — skrót SHA-256 treści zakodowany jako hex małymi literami (dla tekstu: bajty UTF-8 ciągu):

Hashing.hash(of: "https://syncpocket.app")
// "…64 znaki hex małymi literami…"

Gdy pojawia się nowa treść w schowku, magazyn sprawdza hash: jeśli element z takim samym contentHash już istnieje, duplikat nie powstaje — istniejący element wraca na górę, a jego date jest odświeżane. Podpisane buildy iCloud używają CloudKitSyncEngine, a izolowane testy UI — NoopSyncEngine. Wydanie produkcyjne nadal wymaga podpisanego kontenera i testów konfliktów na dwóch urządzeniach.

Retencja czasowa

RetentionPolicy oferuje days30 (domyślne), days60, days90, days120 i forever. Wybrana preferencja jest przechowywana lokalnie poza archiwum. Element wygasa tylko wtedy, gdy jego odświeżone date osiągnie granicę i nie ma żadnej ochrony:

Przypomnienie chroni również po zaplanowanym terminie, dopóki użytkownik go nie wyczyści, dzięki czemu otwarcie doręczonego powiadomienia nie przegrywa z czyszczeniem przy powrocie aplikacji na pierwszy plan.

Migracja

Przy wczytywaniu surowa starsza tablica [ClipItem] i envelope w wersji 1 są migrowane do wersji 2. Brakujące pola dostają zgodne wstecznie wartości, Pinned, Loved i Saved są przywracane, a dane ze starszego katalogu ClipStack są przenoszone automatycznie.