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.