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.
Od tego momentu wszystko dzieje się samo:
- Raycast uruchamia skrypt synchronizacji - samodzielny skrypt trzymany lokalnie na moim komputerze, niewymagający żadnego projektu ani środowiska poza bashem, Pythonem i curlem.
- 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.
- 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.
- 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ę.
- 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):
-
Payload notatek — JSON
{notes: [...], deleted_paths: [...]}gotowy do wysłania na endpoint synchronizacji, - Kolejka załączników — lista plików do osobnego uploadu (ścieżka notatki, ścieżka pliku, typ MIME),
- 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:
- Uwierzytelnienie tokenem.
- Limit częstotliwości.
- 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.