marky

Lightweight bookmarking + plaintext archive
Log | Files | Refs | README | LICENSE

README.md (9604B)


      1 # marky
      2 
      3 A simple web API for bookmarking websites, written in Go. It stores bookmarks in a local SQLite database at `~/.marky/bookmarks.sql`, archives a plaintext rendering of each page, and exposes HTTP endpoints for CRUD and full‑text search. A minimal Vue‑enhanced frontend is embedded and served as a few discrete pages (e.g., `/`, `/u:{username}`, `/settings`).
      4 
      5 - Language: Go
      6 - Storage: SQLite (via `modernc.org/sqlite`, pure Go)
      7 - Default address: `:8080`
      8 - Frontend: Vue 3 (from CDN), embedded static files
      9 
     10 ## Requirements
     11 
     12 - Go 1.24+ (tested with 1.24.5)
     13 - Make (optional, for the provided Makefile)
     14   (Optional) For archiving, if a `w3m` binary is available in `PATH`, marky uses it to render pages to plaintext; otherwise it falls back to the built‑in Go HTML‑to‑text renderer.
     15 
     16 ## Build
     17 
     18 - `make build` — builds `bin/marky`
     19 - Or: `go build -o bin/marky cmd/marky.go`
     20 
     21 ## Run
     22 
     23 - `./bin/marky [options]`
     24 - The database file is created automatically (default: `~/.marky/bookmarks.sql`) and migrations are applied on startup.
     25 - Open the web UI at: `http://localhost:8080/` (or your configured address)
     26 
     27 Flags:
     28 - `--addr` listen address, default `:8080` (e.g., `127.0.0.1:9090`)
     29 - `--db` path to SQLite DB file (created if missing), default resolves to `~/.marky/bookmarks.sql`
     30 - `--disable-signup` disable the `/signup` route and hide signup in the UI
     31 - `--help` show usage and exit
     32 
     33 ## Authentication
     34 
     35 Marky supports session cookies (for the web UI) and API tokens (for scripts, feeds, and tools).
     36 
     37 - Session cookies: set after signup or login in the UI or via endpoints
     38   - POST `/signup` { username, password }
     39   - POST `/login` { username, password }
     40   - POST `/logout`
     41 
     42 - API tokens: per-user credential used with HTTP Basic, header, or query param
     43   - GET `/api/token` → `{ present: true|false }`
     44   - GET `/api/token?reveal=1` → `{ present: true, api_token: "..." }` when set
     45   - POST `/api/token` → `{ api_token: "..." }` (generate/regenerate)
     46 
     47 Use API tokens with requests:
     48 - Basic: `curl -u 'username:API_TOKEN' http://localhost:8080/bookmarks`
     49 - Header: `curl -H 'X-API-Token: API_TOKEN' http://localhost:8080/bookmarks`
     50 - Query: `curl 'http://localhost:8080/bookmarks?token=API_TOKEN'`
     51 
     52 Notes:
     53 - API tokens are unique (partial unique index) and can be regenerated.
     54 - Session cookies and tokens are both accepted; if both are present, either suffices.
     55 
     56 ## API
     57 
     58 ### POST /bookmarks
     59 
     60 Create a new bookmark.
     61 
     62 - Auth: required (session cookie or API token)
     63   - Session: set via `/login` or `/signup`
     64   - API token: provide via one of
     65     - Basic: `-u 'username:API_TOKEN'`
     66     - Header: `X-API-Token: API_TOKEN`
     67     - Query: `?token=API_TOKEN`
     68 
     69 - Request headers:
     70   - `Content-Type: application/json`
     71 - Request body:
     72 ```
     73 {
     74   "url": "https://example.com",              // required
     75   "title": "Example",                        // optional; fetched from page <title> if missing
     76   "description": "Optional description",     // optional
     77   "referrer": "https://referrer.example"     // optional; site that linked here
     78 }
     79 ```
     80 - Responses:
     81   - `201 Created` with JSON body of the created bookmark (archive stored synchronously)
     82   - `200 OK` if the URL already exists; the archive is refreshed and `updated_at` is touched (idempotent by URL)
     83   - `400 Bad Request` for invalid payload
     84   - `415 Unsupported Media Type` if not JSON
     85   - `422 Unprocessable Entity` if archiver fails to fetch content
     86   - `500 Internal Server Error` on unexpected errors
     87 - Response body:
     88 ```
     89 {
     90   "id": 1,
     91   "url": "https://example.com",
     92   "title": "Example",
     93   "description": "Optional description",
     94   "referrer": "https://referrer.example",
     95   "created_at": "2025-01-01T12:34:56Z",
     96   "updated_at": "2025-01-01T12:34:56Z"
     97 }
     98 ```
     99 - curl example:
    100 ```
    101 curl -i -X POST http://localhost:8080/bookmarks \
    102   -H 'Content-Type: application/json' \
    103   -d '{"url":"https://example.com","title":"Example","description":"Sample"}'
    104 ```
    105 
    106 ### GET /bookmarks
    107 
    108 List the authenticated user’s bookmarks in reverse chronological order, or search with full‑text. When `q` is provided, results include an `excerpt` with highlighted matches. The endpoint can return JSON (default) or RSS.
    109 
    110 - Auth: required (session cookie or API token). Returns `401 Unauthorized` when unauthenticated.
    111 - Query params:
    112   - `limit` (optional, default 50, max 200)
    113   - `offset` (optional, default 0)
    114   - `q` (optional) full-text query across title, url, description, and archived content
    115   - `format=rss` (optional) to return an RSS 2.0 feed
    116 - Responses:
    117   - `200 OK` with JSON array of bookmarks or RSS feed
    118   - `400 Bad Request` for invalid `limit` or `offset`
    119   - `500 Internal Server Error` on unexpected errors
    120 - curl examples (session or token required for private data):
    121 ```
    122 curl -s -u 'username:API_TOKEN' http://localhost:8080/bookmarks | jq
    123 curl -s -u 'username:API_TOKEN' 'http://localhost:8080/bookmarks?limit=20&offset=20' | jq
    124 curl -s -u 'username:API_TOKEN' 'http://localhost:8080/bookmarks?q=example' | jq
    125 curl -s -u 'username:API_TOKEN' 'http://localhost:8080/bookmarks?format=rss' | xmllint --format -
    126 curl -s -u 'username:API_TOKEN' 'http://localhost:8080/bookmarks.xml?q=example' | xmllint --format -
    127 ```
    128 
    129 Notes on search excerpts:
    130 - `excerpt` highlights matches using guillemets, e.g., `… some «example» text …`.
    131 - Ellipses `…` indicate omitted surrounding text, and snippets focus on the archived page content.
    132 
    133 ### PUT /bookmarks/{id}
    134 
    135 Update a bookmark’s URL, title, and/or description.
    136 
    137 - Auth: required (session cookie or API token)
    138 - Request headers:
    139   - `Content-Type: application/json`
    140 - Request body:
    141 ```
    142 {
    143   "url": "https://example.com",     // required
    144   "title": "New Title",              // required
    145   "description": "Updated notes"     // optional (omitted preserves existing)
    146 }
    147 ```
    148 - Responses:
    149   - `200 OK` with updated bookmark JSON
    150   - `400 Bad Request` for invalid payload
    151   - `404 Not Found` if the bookmark ID does not exist
    152   - `500 Internal Server Error` on unexpected errors
    153 - curl example:
    154 ```
    155 curl -s -X PUT http://localhost:8080/bookmarks/1 \
    156   -H 'Content-Type: application/json' \
    157   -d '{"url":"https://example.com","title":"Updated","description":"Notes"}' | jq
    158 ```
    159 
    160 ### DELETE /bookmarks/{id}
    161 
    162 Delete a bookmark (and its search index entry). Archived text is removed from search index but remains in `archives` table only if you retain rows; this implementation removes the FTS entry, and the foreign key setup will cascade archives removal if configured via migrations.
    163 
    164 - Auth: required (session cookie or API token)
    165 - Responses:
    166   - `204 No Content` on success
    167   - `404 Not Found` if the bookmark ID does not exist
    168   - `500 Internal Server Error` on unexpected errors
    169 - curl example:
    170 ```
    171 curl -i -X DELETE http://localhost:8080/bookmarks/1
    172 ```
    173 
    174 ### Per‑user feeds
    175 
    176 Feeds are normalized under `/u:{username}(.format)` where `format` is `json` or `rss`. When no format is provided, HTML is served.
    177 
    178 - Examples (include private when authenticated):
    179   - Basic: `curl -u 'dce:API_TOKEN' http://localhost:8080/u:dce.rss`
    180   - Header: `curl -H 'X-API-Token: API_TOKEN' http://localhost:8080/u:dce.json`
    181   - Query: `curl 'http://localhost:8080/u:dce.json?token=API_TOKEN'`
    182 
    183 Notes:
    184 - Unauthenticated or non‑owner requests return only public bookmarks.
    185 - Owner (session, Basic auth, header, or query token) sees all bookmarks (public + private).
    186 
    187 ### Bookmarklet
    188 
    189 - Open `/settings` while logged in to get a “Save to Marky” bookmarklet you can drag to your bookmarks bar.
    190 - Clicking the bookmarklet on any page opens a small popup with URL and title prefilled; you can adjust description and toggle public/private.
    191 - The popup submits to `/bookmarks` using your session or API token (embedded when present).
    192 
    193 ## Data Model
    194 
    195 Table: `bookmarks`
    196 - `id` INTEGER PRIMARY KEY AUTOINCREMENT (starts at 1)
    197 - `user_id` INTEGER NOT NULL REFERENCES `users(id)`
    198 - `url` TEXT NOT NULL (unique per user: `(user_id, url)`)
    199 - `title` TEXT NOT NULL
    200 - `description` TEXT NULL
    201 - `public` INTEGER NOT NULL DEFAULT 0
    202 - `created_at` TEXT NOT NULL (RFC3339)
    203 - `updated_at` TEXT NOT NULL (RFC3339)
    204 
    205 Migrations are embedded and applied automatically on startup.
    206 
    207 An `archives` table stores the plaintext dump per bookmark and is kept in sync on creation.
    208 
    209 ### GET /bookmarks/{id}/archive
    210 
    211 Return the archived plaintext for the given bookmark ID.
    212 
    213 - Auth: not required (returns plain text if the archive exists)
    214 - Responses:
    215   - `200 OK` with `text/plain; charset=utf-8`
    216   - `404 Not Found` if no archive exists for the bookmark
    217   - `500 Internal Server Error` on unexpected errors
    218 
    219 curl example:
    220 ```
    221 curl -s http://localhost:8080/bookmarks/1/archive
    222 ```
    223 
    224 ## Development
    225 
    226 - Code entrypoint: `cmd/marky.go`
    227 - Database and helpers: `internal/`
    228 - Migrations: `internal/migrations/*.sql`
    229 - Archiver: `internal/archiver.go` (pure-Go HTML-to-text)
    230 - Embedded web UI: `internal/web/` (discrete pages)
    231 
    232 ### Reverse proxy / HTTPS
    233 
    234 - When running behind a TLS-terminating proxy (e.g., Cloudflare, nginx), the app
    235   infers the public origin from standard headers and generates correct `https://`
    236   links for the bookmarklet and feeds.
    237 - Recognized headers: `X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Forwarded-Scheme`,
    238   `X-Forwarded-Ssl`, `Forwarded`, and Cloudflare's `Cf-Visitor`.
    239 - To force a specific origin, set `--base-url` (e.g., `marky --base-url https://bookmarks.example.com`) or the env var `MARKY_BASE_URL`.
    240 
    241 ---
    242 
    243 ## Acknowledgments
    244 
    245 This project was developed with the assistance of [ChatGPT][1] for code generation.
    246 
    247 [1]: https://openai.com/chatgpt
    248 
    249 ## License
    250 
    251 MIT License -- see LICENSE for details.