Synchronizacja notatek Obsidian
Dev

Dowiedz się więcej o mojej synchronizacji notatek.

Dlaczego poświęciłem na to swój czas?

Notatki są przydatne. Przez długi czas korzystałem z Notion, które sprawdzało się dobrze. Było wygodne przede wszystkim dlatego, że pozwalało szybko opublikować notatkę bezpośrednio z aplikacji desktopowej. Taka notatka trafiała pod dedykowany adres i była publicznie dostępna jako strona internetowa.

Z czasem jednak coraz mocniej odczuwałem ograniczenia tego rozwiązania:

  • Brak realnej kontroli nad udostępnionymi notatkami  - po publikacji notatka żyła na infrastrukturze Notion, na zasadach Notion.
  • Brak możliwości publikacji za hasłem - nie dało się udostępnić notatki publicznie, ale schowanej za prostym wallem odblokowywanym hasłem.
  • Brak personalizacji - wygląd i sposób prezentacji notatek były z góry narzucone.
  • Brak sensownego kategoryzowania, segregacji i szybkiego filtrowania opublikowanych notatek.

Szukając alternatywy, natrafiłem na opinię, że w dobie ai to Obsidian jest narzędziem, które ma szansę stać się podstawowym elementem pracy każdego szanującego się programisty. Ma to sens: notatki w Obsidianie to zwykłe pliki .md, a Markdown okazał się idealnym formatem przekazywania wiedzy modelom - pisze się w nim reguły i instrukcje dla agentów, dokumentację projektów czy pliki kontekstowe. Notatka w Markdownie jest więc jednocześnie treścią dla człowieka i wsadem dla maszyny.

Drugim decydującym argumentem jest architektura Obsidiana: vault z notatkami to po prostu katalog na lokalnym dysku. Pełny dostęp do własnych plików daje ogromną swobodę budowania customowych rozwiązań i pozwala rozwiązać wszystkie wymienione wyżej bolączki dokładnie tak, jak sam tego chcę.

Właśnie z tego połączenia powstała strona, na której czytasz tę notatkę. Zbudowałem system, który po jednym naciśnięciu klawisza synchronizuje wybrane notatki z mojego lokalnego vaultu na tę stronę. Daję mu pełną kontrolę nad tym, co jest publiczne: notatki mogę segregować, filtrować i przeszukiwać, a dostęp do wybranych z nich zabezpieczyć hasłem.

Jak działa cały proces?

Opis bez wchodzenia w śrubki. Jeśli interesują Cię techniczne sprawy to omówienie każdego elementu, od logiki skryptu po zabezpieczenia API, znajdziesz w następnych sekcjach tej notatki.

Kluczowym elementem uproszczenia całego procesu było spięcie go z Raycastem, czyli macOSowym spotlightem na sterydach. Raycast pozwala podpiąć własne skrypty jako script commands i uruchamiać je błyskawicznie z poziomu klawiatury, z dowolnego miejsca w systemie. Dzięki temu publikacja notatek nie wymaga otwierania terminala, projektu ani niczego innego poza dwoma czynnościami:
cmd + spacja, wpisanie „Sync Obsidian", Enter
Tyle.

Nagranie z ekranu 2026-07-11 o 18.37.27.gif

Od tego momentu wszystko dzieje się samo:

  1. Raycast uruchamia skrypt synchronizacji - samodzielny skrypt trzymany lokalnie na moim komputerze, niewymagający żadnego projektu ani środowiska poza bashem, Pythonem i curlem.
  2. Skrypt analizuje mój vault Obsidiana - przechodzi wszystkie notatki i porównuje je z zapamiętanym stanem poprzedniej synchronizacji. Dzięki sumom kontrolnym wie dokładnie, które notatki zmieniłem, które dodałem, a które usunąłem. Niezmienione pliki w ogóle nie biorą udziału w dalszym procesie. Synchronizacja jest przyrostowa.
  3. Zmiany lecą na serwer - skrypt wysyła do API mojej strony jeden request z treścią zmienionych notatek i listą usuniętych, a następnie osobno dogrywa obrazki i inne załączniki osadzone w notatkach. Całość jest uwierzytelniana tokenem.
  4. Serwer przetwarza notatki - nowe zapisuje, zmienione nadpisuje, usunięte oznacza jako skasowane. Świeżo zsynchronizowana notatka nigdy nie jest od razu publiczna. Trafia do bazy jako ukryta i czeka na moją decyzję.
  5. Publikacja to świadomy, osobny krok — w panelu administracyjnym strony decyduję, którą notatkę upublicznić, dodaję jej krótki opis, a w razie potrzeby zabezpieczam ją hasłem. Widoczność i hasło są od siebie niezależne: notatka może być publiczna i otwarta, publiczna za hasłem, albo w ogóle ukryta.

Efekt końcowy: synchronizacja notatek zajmuje mniej niż 2 sekundy. Teoretycznie ten skrypt mógłby być uruchamiany cyklicznie bez mojego udziału, ale nie zależało mi na tym


Flow synchronizacji - opis techniczny


Etap 1 - skrypt Python

Rola i odpowiedzialność

Program w Pythonie, osadzony w skrypcie bashowym, jest mózgiem synchronizacji, ale nie jej wykonawcą. Nie wykonuje żadnej komunikacji sieciowej - jego jedynym zadaniem jest odpowiedź na pytanie: „co zmieniło się w vaulcie od ostatniej udanej synchronizacji i co w związku z tym trzeba wysłać?". Wynik tej analizy zapisuje w trzech plikach tymczasowych, które konsumuje dalsza, transportowa część skryptu (curl).

Ten podział odpowiedzialności jest fundamentem niezawodności całego mechanizmu: analiza jest czysta (czyta dysk, niczego nie modyfikuje poza plikami tymczasowymi), a decyzja o utrwaleniu nowego stanu zapada dopiero po potwierdzonym sukcesie transportu.

Wejście i wyjście

Wejście:

  • katalog vaultu Obsidian (OBSIDIAN_VAULT_PATH),
  • plik stanu z poprzedniej synchronizacji (domyślnie ~/.obsidian_sync_state.json) — JSON przechowujący dla każdej notatki jej sumę SHA-256, czas modyfikacji i tytuł, a dla każdej pary notatka–załącznik sumę SHA-256 załącznika.

Wyjście - (trzy pliki):

  1. Payload notatek — JSON {notes: [...], deleted_paths: [...]} gotowy do wysłania na endpoint synchronizacji,
  2. Kolejka załączników — lista plików do osobnego uploadu (ścieżka notatki, ścieżka pliku, typ MIME),
  3. Kandydat na nowy stan — pełny obraz obecnego vaultu, który zostanie utrwalony jako plik stanu tylko wtedy, gdy cała wysyłka się powiedzie.

Jak działa

1. Inwentaryzacja vaultu. Program przechodzi rekurencyjnie cały katalog vaultu i buduje indeks wszystkich plików, w tym słownik „nazwa pliku → ścieżki" służący później do rozwiązywania embedów po samej nazwie.

2. Analiza każdej notatki. Dla każdego pliku .md/.markdown liczona jest suma kontrolna SHA-256 całej zawartości (strumieniowo, bez ładowania pliku do pamięci w całości). Treść jest rozdzielana na frontmatter YAML i właściwe body — frontmatter parsowany jest uproszczonym parserem (pary klucz–wartość i listy inline) i dalej podróżuje jako osobny obiekt, nigdy jako część treści. Tytuł notatki wyprowadzany jest z pola title frontmattera, a przy jego braku — z nazwy pliku.

3. Decyzja o wysyłce (diff). Notatka trafia do payloadu tylko wtedy, gdy w zapamiętanym stanie nie istnieje, albo zmieniła się jej suma kontrolna lub tytuł. Czas modyfikacji pliku jest zapisywany informacyjnie, ale nie wpływa na decyzję — rozstrzyga wyłącznie faktyczna zawartość. To czyni synchronizację przyrostową na poziomie plików: niezmienione notatki w ogóle nie są wysyłane, natomiast zmieniona notatka jest wysyłana zawsze w całości (mechanizm nie robi diffów wewnątrz treści).

4. Wykrywanie usunięć. Lista usunięć to prosta różnica zbiorów: ścieżki obecne w poprzednim stanie, których nie ma już na dysku. Program nie odróżnia usunięcia od zmiany nazwy — plik przeniesiony lub przemianowany jest widziany jako „stary usunięty + nowy utworzony".

5. Załączniki (embedy). W body każdej notatki wyszukiwane są embedy Obsidiana (<code>embedded: ...</code>). Każdy embed jest rozwiązywany do konkretnego pliku trzystopniowo: najpierw jako ścieżka względna od katalogu notatki, potem od korzenia vaultu, na końcu po unikalnej nazwie pliku w całym vaulcie (przy niejednoznaczności preferowany jest plik z katalogu notatki). Rozwiązany załącznik trafia do kolejki uploadu tylko wtedy, gdy zmieniła się jego suma kontrolna względem zapamiętanego stanu — kluczem jest para notatka–plik.

Właściwości gwarantowane przez tę konstrukcję

  • Przyrostowość — przez sieć lecą wyłącznie realnie zmienione pliki; koszt niezmienionego vaultu to samo skanowanie dysku.
  • Odporność na przerwanie — stan jest utrwalany dopiero po pełnym sukcesie transportu, więc nieudana synchronizacja zostanie w całości ponowiona przy następnym uruchomieniu; nic nie ginie.
  • Samowystarczalność pierwszego uruchomienia — brak lub uszkodzenie pliku stanu oznacza po prostu pełną synchronizację całego vaultu.
  • Determinizm identyfikacji — jedynym identyfikatorem notatki jest jej ścieżka względna w vaulcie; serwer używa jej jako klucza upsertu.

Etap 2 - obsługa requesta po stronie serwera

Rola i odpowiedzialność

Po stronie Rails synchronizację obsługuje dedykowany kontroler API wystawiający dwa endpointy: jeden przyjmujący zbiorczy payload notatek wraz z listą usunięć, drugi przyjmujący pojedyncze załączniki. Jego odpowiedzialność można streścić jedną zasadą: przyjąć zmiany z zaufanego źródła, zapisać je idempotentnie i niczego samodzielnie nie publikować. Serwer traktuje klienta jako źródło treści, ale nie decyzji — wszystko, co dotyczy widoczności i ochrony notatek, pozostaje wyłącznie po stronie panelu administracyjnego.

Bramka wejściowa — zanim request dotknie danych

Każdy request przechodzi przez trzy niezależne zabezpieczenia:

  1. Uwierzytelnienie tokenem.
  2. Limit częstotliwości.
  3. Limity rozmiaru.

Przetwarzanie notatek — idempotentny upsert

Dla każdej notatki z payloadu serwer wykonuje tę samą sekwencję:

  • Normalizacja ścieżki. Ścieżka źródłowa jest czyszczona i odrzucana, jeśli próbuje wyjść poza swój obszar. Znormalizowana ścieżka jest jedynym kluczem tożsamości notatki.
  • Upsert. Notatka o danej ścieżce jest znajdowana lub tworzona. Treść, frontmatter, tagi, suma kontrolna i czas modyfikacji są nadpisywane w całości. Serwer nie porównuje wersji, ufa, że klient przysłał notatkę, bo ta się zmieniła.
  • Ochrona decyzji administratora. Nowa notatka rodzi się zawsze jako ukryta.
  • Zmartwychwstanie. Każdy upsert zeruje znacznik usunięcia. Notatka skasowana z vaultu i później przywrócona pod tą samą ścieżką wraca do życia z zachowaną widocznością, opisem i hasłem.

Usunięcia są równie proste: wszystkie ścieżki z listy deleted_paths dostają jednym zapytaniem znacznik czasu usunięcia (soft delete). Rekordy i ich załączniki fizycznie zostają w bazie — znikają jedynie ze wszystkich publicznych i administracyjnych widoków. Nic nie jest kasowane nieodwracalnie.

Przetwarzanie załączników

Załączniki przychodzą na osobny endpoint, każdy w osobnym requeście, już po zsynchronizowaniu notatek — dzięki temu serwer zawsze może przypiąć plik do istniejącej notatki (wskazanej tą samą ścieżką źródłową; brak notatki to błąd 404). Plik przechodzi walidację rozmiaru i typu (whitelist: PNG, JPEG, WebP, GIF, PDF, zwykły tekst) i trafia do Active Storage jako załącznik notatki. Endpoint przyjmuje dwa formaty: klasyczny multipart (używany przez skrypt) oraz wariant base64 w JSON — furtkę dla innych klientów. Przy renderowaniu notatki embedy Obsidiana są dopasowywane do załączników po nazwie pliku i zamieniane na właściwe znaczniki HTML (obraz, wideo, audio lub osadzony PDF).

Właściwości tej konstrukcji

  • Idempotencja - ten sam payload można przysłać dowolną liczbę razy; stan końcowy bazy będzie identyczny (wyjątkiem są załączniki, które przy ponownym uploadzie dopinane są obok istniejących).
  • Rozdzielenie treści od decyzji - synchronizacja zarządza wyłącznie treścią; widoczność i hasła należą do panelu administracyjnego i żadna operacja przychodząca z zewnątrz nie jest w stanie ich zmienić.

Etap 3 - obsługa po stronie panelu administratora

Panel administracyjny (dostępny tylko dla admina) to jedyne miejsce, w którym zapadają decyzje o publikacji — synchronizacja dostarcza treść, ale niczego sama nie upublicznia. Dla każdej notatki panel pozwala na trzy niezależne rzeczy:

  • włączyć lub wyłączyć widoczność — każda świeżo zsynchronizowana notatka jest domyślnie ukryta,
  • dodać krótki opis wyświetlany na publicznej liście notatek,
  • ustawić, zmienić lub zdjąć hasło dostępu

Widoczność i hasło są od siebie niezależne, co daje trzy stany notatki: ukryta, publiczna otwarta, publiczna za hasłem.