diff --git a/main.go b/main.go index 13dcced..7932212 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,9 @@ import ( "time" ) +// -------------------------------------------------------------------- +// Configuration: HTTP client and buffer pool (memory tuned) +// -------------------------------------------------------------------- var httpClient = &http.Client{ Timeout: 15 * time.Second, Transport: &http.Transport{ @@ -36,7 +39,7 @@ var httpClient = &http.Client{ var copyBufPool = sync.Pool{ New: func() any { - b := make([]byte, 32*1024) + b := make([]byte, 32*1024) // 32KB buffer reused return &b }, } @@ -44,39 +47,63 @@ var copyBufPool = sync.Pool{ const pinterestSearchURL = "https://www.pinterest.com/resource/BaseSearchResource/get/" const cookieName = "pinata_bm" -// -- bookmark encryption key (32 bytes) -- +// -------------------------------------------------------------------- +// Bookmarks: types, key, limits +// -------------------------------------------------------------------- +type BookmarkEntry struct { + Type string `json:"type"` // "q" or "img" + Value string `json:"value"` // query or image URL +} + var bookmarkKey []byte var bookmarkingEnabled bool var disableReverse bool -func init() { - if kb := os.Getenv("PINATA_BOOKMARK_KEY"); kb != "" { - if decoded, err := base64.StdEncoding.DecodeString(kb); err == nil && len(decoded) == 32 { - bookmarkKey = decoded - bookmarkingEnabled = true - log.Println("Bookmarking enabled") - } else { - log.Println("PINATA_BOOKMARK_KEY provided but invalid; bookmarking disabled") - bookmarkingEnabled = false - } - } else { - log.Println("PINATA_BOOKMARK_KEY not set; bookmarking disabled") - bookmarkingEnabled = false - } +const maxBookmarks = 30 +const maxItemLen = 256 - // New: whether to disable reverse image search (Tineye) - disableEnv := strings.ToLower(strings.TrimSpace(os.Getenv("PINATA_DISABLE_REVERSE"))) - if disableEnv == "1" || disableEnv == "true" || disableEnv == "yes" { - disableReverse = true - log.Println("Reverse image search disabled via PINATA_DISABLE_REVERSE") - } else { - disableReverse = false - } +// -------------------------------------------------------------------- +// init: read env variables +// - PINATA_BOOKMARK_KEY: base64 32-byte key to enable bookmarks +// - PINATA_DISABLE_REVERSE: "1"/"true"/"yes" disables reverse search link +// -------------------------------------------------------------------- +func init() { + // PINATA_BOOKMARK_KEY + if kb := os.Getenv("PINATA_BOOKMARK_KEY"); kb != "" { + if decoded, err := base64.StdEncoding.DecodeString(kb); err == nil && len(decoded) == 32 { + bookmarkKey = decoded + bookmarkingEnabled = true + log.Println("Bookmarking enabled") + } else { + bookmarkingEnabled = false + log.Println("PINATA_BOOKMARK_KEY provided but invalid; bookmarking disabled") + } + } else { + bookmarkingEnabled = false + log.Println("PINATA_BOOKMARK_KEY not set; bookmarking disabled") + } + + // PINATA_DISABLE_REVERSE + switch strings.ToLower(strings.TrimSpace(os.Getenv("PINATA_DISABLE_REVERSE"))) { + case "1", "true", "yes": + disableReverse = true + log.Println("Reverse image search disabled via PINATA_DISABLE_REVERSE") + default: + disableReverse = false + } } -// encrypt a JSON list of strings -> base64 url safe -func encryptBookmarks(items []string) (string, error) { - plain, _ := json.Marshal(items) +// -------------------------------------------------------------------- +// Encryption helpers for cookie storage (AES-GCM) +// -------------------------------------------------------------------- +func encryptBookmarks(entries []BookmarkEntry) (string, error) { + if !bookmarkingEnabled { + return "", nil + } + plain, err := json.Marshal(entries) + if err != nil { + return "", err + } block, err := aes.NewCipher(bookmarkKey) if err != nil { return "", err @@ -89,12 +116,14 @@ func encryptBookmarks(items []string) (string, error) { if _, err := rand.Read(nonce); err != nil { return "", err } - ciphertext := gcm.Seal(nonce, nonce, plain, nil) - return base64.RawURLEncoding.EncodeToString(ciphertext), nil + ct := gcm.Seal(nonce, nonce, plain, nil) + return base64.RawURLEncoding.EncodeToString(ct), nil } -// decrypt base64 cookie -> list of strings -func decryptBookmarks(encoded string) ([]string, error) { +func decryptBookmarks(encoded string) ([]BookmarkEntry, error) { + if !bookmarkingEnabled { + return nil, nil + } data, err := base64.RawURLEncoding.DecodeString(encoded) if err != nil { return nil, err @@ -109,7 +138,7 @@ func decryptBookmarks(encoded string) ([]string, error) { } ns := gcm.NonceSize() if len(data) < ns { - return nil, err + return nil, io.ErrUnexpectedEOF } nonce := data[:ns] ct := data[ns:] @@ -117,15 +146,27 @@ func decryptBookmarks(encoded string) ([]string, error) { if err != nil { return nil, err } - var items []string - if err := json.Unmarshal(plain, &items); err != nil { - return nil, err + // Try new format ([]BookmarkEntry) + var entries []BookmarkEntry + if err := json.Unmarshal(plain, &entries); err == nil { + return entries, nil } - return items, nil + // Fallback to legacy []string -> convert to type "q" + var arr []string + if err := json.Unmarshal(plain, &arr); err == nil { + out := make([]BookmarkEntry, 0, len(arr)) + for _, s := range arr { + out = append(out, BookmarkEntry{Type: "q", Value: s}) + } + return out, nil + } + return nil, io.ErrUnexpectedEOF } -// helper to read bookmarks from request cookie (returns empty slice if none or invalid) -func readBookmarksFromReq(r *http.Request) []string { +// -------------------------------------------------------------------- +// Cookie helpers: read, set, clear +// -------------------------------------------------------------------- +func readBookmarksFromReq(r *http.Request) []BookmarkEntry { if !bookmarkingEnabled { return nil } @@ -133,68 +174,72 @@ func readBookmarksFromReq(r *http.Request) []string { if err != nil || c.Value == "" { return nil } - items, err := decryptBookmarks(c.Value) + entries, err := decryptBookmarks(c.Value) if err != nil { + // invalid cookie -> ignore return nil } - return items + return entries } -// helper to set bookmarks cookie -func setBookmarksCookie(w http.ResponseWriter, items []string) { +func setBookmarksCookie(w http.ResponseWriter, entries []BookmarkEntry) { if !bookmarkingEnabled { return } - // sanitize and truncate each item - trunc := make([]string, 0, len(items)) - for _, s := range items { - s = strings.TrimSpace(s) - if s == "" { + seen := map[string]bool{} + out := make([]BookmarkEntry, 0, len(entries)) + for _, e := range entries { + v := strings.TrimSpace(e.Value) + if v == "" { continue } - if len(s) > 64 { - s = s[:64] + if len(v) > maxItemLen { + v = v[:maxItemLen] } - trunc = append(trunc, s) - if len(trunc) >= 30 { + key := e.Type + "|" + v + if seen[key] { + continue + } + seen[key] = true + if e.Type != "q" && e.Type != "img" { + e.Type = "q" + } + out = append(out, BookmarkEntry{Type: e.Type, Value: v}) + if len(out) >= maxBookmarks { break } } - enc, err := encryptBookmarks(trunc) + enc, err := encryptBookmarks(out) if err != nil { - // fail silently (do not set) + // fail silently return } c := &http.Cookie{ Name: cookieName, Value: enc, Path: "/", - HttpOnly: true, // not accessible to JS (we have no JS but keeps it private) + HttpOnly: true, SameSite: http.SameSiteLaxMode, - // Secure: true, // keep commented for local HTTP dev - set true in production behind TLS - MaxAge: 60 * 60 * 24 * 365 * 10, // ~10 years + // Secure: true, // enable when serving over HTTPS + MaxAge: 60 * 60 * 24 * 365 * 10, } http.SetCookie(w, c) } -// remove bookmark and reset cookie -func removeBookmarkCookie(w http.ResponseWriter, items []string) { - // if empty, clear cookie - if len(items) == 0 { - c := &http.Cookie{ - Name: cookieName, - Value: "", - Path: "/", - HttpOnly: true, - MaxAge: -1, - } - http.SetCookie(w, c) - return +func clearBookmarksCookie(w http.ResponseWriter) { + c := &http.Cookie{ + Name: cookieName, + Value: "", + Path: "/", + HttpOnly: true, + MaxAge: -1, } - setBookmarksCookie(w, items) + http.SetCookie(w, c) } -// ----- CSS and HTML (no JS) ----- +// -------------------------------------------------------------------- +// Stylesheet and minimal HTML templates (no JS) +// -------------------------------------------------------------------- const cssContent = ` :root{--bg:#0b0f17;--muted:#94a3b8;--text:#e6e6ff;--accent:#7c3aed;--card-shadow:rgba(0,0,0,0.6)} *{box-sizing:border-box}html,body{height:100%}body{margin:0;padding:20px;background:linear-gradient(180deg,#071020 0%,#0b0f17 100%);color:var(--text);font-family:ui-monospace,Menlo,Monaco,monospace} @@ -210,29 +255,22 @@ button[type="submit"],.btn-save{background:linear-gradient(90deg,var(--accent),# .img-container { column-width: 260px; column-gap: 16px; width: 100%; max-width: 1400px; margin-top: 18px; } .card { display:inline-block; width:100%; margin:0 0 16px; border-radius:10px; overflow:hidden; background:linear-gradient(180deg,rgba(255,255,255,0.01),rgba(255,255,255,0.02)); box-shadow:0 6px 18px rgba(3,7,18,0.6); border:1px solid rgba(124,58,237,0.06); break-inside: avoid; -webkit-column-break-inside: avoid; -moz-column-break-inside: avoid; min-height:0; position:relative; } .card img { display:block; width:100%; height:auto; object-fit:cover; background:#08101a; } -.magnifier{position:absolute;top:8px;right:8px;background:rgba(0,0,0,0.45);border:1px solid rgba(255,255,255,0.06);color:var(--text);padding:6px;border-radius:999px;font-size:14px;width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;text-decoration:none;transition:transform .12s ease,background .12s ease} -.magnifier:hover{transform:translateY(-2px);background:linear-gradient(180deg,rgba(124,58,237,0.14),rgba(124,58,237,0.08));color:white} +.card-controls { position:absolute; top:8px; right:8px; display:flex; gap:8px; align-items:center; } +.btn-save-mini { background: rgba(124,58,237,0.12); color: var(--text); border: none; padding:6px; border-radius:999px; cursor:pointer; font-weight:700; } +.magnifier{background:rgba(0,0,0,0.45);border:1px solid rgba(255,255,255,0.06);color:var(--text);padding:6px;border-radius:999px;font-size:14px;width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;text-decoration:none} .bookmarks{margin-left:12px;color:var(--muted);font-size:14px} .bookmark-list{margin-top:10px;display:flex;gap:8px;flex-wrap:wrap} .bookmark-pill{background:rgba(255,255,255,0.03);padding:6px 8px;border-radius:999px;border:1px solid rgba(255,255,255,0.04);font-size:13px;display:flex;gap:6px;align-items:center} .bookmark-pill form{display:inline} .bookmark-remove-btn{background:transparent;border:none;color:#ff7b7b;font-weight:700;cursor:pointer;padding:0 6px} +.export-form{margin-top:12px;display:flex;gap:8px;align-items:center} .pagination{text-align:center;margin:26px 0} .pagination a{color:var(--accent);text-decoration:none;padding:8px 12px;border-radius:8px;border:1px solid rgba(124,58,237,0.12);background:rgba(124,58,237,0.02)} .footer-note{color:var(--muted);font-size:12px;margin-top:22px} -@media (max-width:640px){ - body{padding:12px;font-size:18px} - .brand{font-size:22px} - input[type="text"]{min-width:120px;padding:12px 14px;font-size:16px} - button[type="submit"],.btn-save{padding:10px 14px;font-size:16px;border-radius:10px} - .img-container{column-width:180px;column-gap:12px} - .search-block{gap:10px;flex-direction:column} - .search-inline{width:100%} - .search-box{margin-left:0;width:100%} - .bookmarks{order:3;width:100%;margin-top:8px} -} +@media (max-width:640px){ body{padding:12px;font-size:18px} .brand{font-size:22px} input[type="text"]{min-width:120px;padding:12px 14px;font-size:16px} button[type="submit"],.btn-save{padding:10px 14px;font-size:16px;border-radius:10px} .img-container{column-width:180px;column-gap:12px} .search-block{gap:10px;flex-direction:column} .search-inline{width:100%} .search-box{margin-left:0;width:100%} .bookmarks{order:3;width:100%;margin-top:8px} } ` +<<<<<<< HEAD // Static index page (no JS). Bookmarks rendered server-side only here. func indexHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf8") @@ -258,9 +296,50 @@ func indexHandler(w http.ResponseWriter, r *http.Request) { } io.WriteString(w, ``) +======= +// -------------------------------------------------------------------- +// Handlers +// -------------------------------------------------------------------- +func styleHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/css; charset=utf-8") + _, _ = io.WriteString(w, cssContent) +>>>>>>> cc7ebb9 (image bookmarking, bookmarks export/import) } -// Search streaming handler (same streaming approach as before), includes server-side Save form when bookmarkingEnabled +// Index (front page) - server-rendered bookmarks and import/export forms +func indexHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + // header and search block + _, _ = io.WriteString(w, `Pinata - Search`) + _, _ = io.WriteString(w, `
Pinata
`) + _, _ = io.WriteString(w, `
Search images from Pinterest — submit a search to view results.
`) + _, _ = io.WriteString(w, `
`) + + // bookmarks shown only on index + if bookmarkingEnabled { + items := readBookmarksFromReq(r) + _, _ = io.WriteString(w, `
Saved bookmarks
`) + for _, e := range items { + escaped := html.EscapeString(e.Value) + if e.Type == "q" { + _, _ = io.WriteString(w, ``+escaped+``) + } else { + _, _ = io.WriteString(w, ``+escaped+``) + } + _, _ = io.WriteString(w, `
`) + } + _, _ = io.WriteString(w, `
`) + + // export and import + _, _ = io.WriteString(w, `
`) + _, _ = io.WriteString(w, `
`) + _, _ = io.WriteString(w, `
`) + } + + _, _ = io.WriteString(w, ``) +} + +// Streaming search handler (no JS). Writes cards as results are decoded. func searchHandler(w http.ResponseWriter, r *http.Request) { q := strings.TrimSpace(r.URL.Query().Get("q")) if len(q) < 1 || len(q) > 64 { @@ -315,22 +394,19 @@ func searchHandler(w http.ResponseWriter, r *http.Request) { } } - // start streaming HTML + // Start streaming HTML w.Header().Set("Content-Type", "text/html; charset=utf-8") - io.WriteString(w, ``+html.EscapeString(q)+` - Pinata`) - // header with inline search and server-side Save form (if enabled) - io.WriteString(w, `
Pinata`) + _, _ = io.WriteString(w, `

Results for "`+html.EscapeString(q)+`"

`) + _, _ = io.WriteString(w, `
`) dec := json.NewDecoder(resp.Body) var nextBookmark string @@ -376,18 +452,26 @@ func searchHandler(w http.ResponseWriter, r *http.Request) { } esc := url.QueryEscape(u) b64 := base64.StdEncoding.EncodeToString([]byte(u)) - var cardBuilder strings.Builder - cardBuilder.WriteString(`
image`) + + // Write card (with magnifier optional and Save image form) + _, _ = io.WriteString(w, `
`) + _, _ = io.WriteString(w, `image`) + _, _ = io.WriteString(w, `
`) if !disableReverse { - cardBuilder.WriteString(`🔍`) + _, _ = io.WriteString(w, `🔍`) } - cardBuilder.WriteString(`
`) - io.WriteString(w, cardBuilder.String()) + if bookmarkingEnabled { + next := "/search?q=" + url.QueryEscape(q) + _, _ = io.WriteString(w, `
`) + } + _, _ = io.WriteString(w, `
`) // card-controls + _, _ = io.WriteString(w, `
`) // card + if f, ok := w.(http.Flusher); ok { f.Flush() } } - _, _ = dec.Token() // consume closing + _, _ = dec.Token() case "bookmark": t, err := dec.Token() if err == nil { @@ -400,10 +484,8 @@ func searchHandler(w http.ResponseWriter, r *http.Request) { } } - // close container - io.WriteString(w, `
`) - - // show pagination link if present + // finish results + _, _ = io.WriteString(w, `
`) if nextBookmark != "" { qenc := url.QueryEscape(q) benc := url.QueryEscape(nextBookmark) @@ -414,13 +496,15 @@ func searchHandler(w http.ResponseWriter, r *http.Request) { cenc = "&csrftoken=" + url.QueryEscape(csrftoken) } next := "/search?q=" + qenc + "&bookmark=" + benc + cenc - io.WriteString(w, ``) + _, _ = io.WriteString(w, ``) } - - io.WriteString(w, ``) + _, _ = io.WriteString(w, ``) } -// POST /bookmark : server-side saving of a bookmark (no JS). expects form q and optional next. +// -------------------------------------------------------------------- +// Bookmark endpoints (server-side, no JS) +// -------------------------------------------------------------------- + func bookmarkPostHandler(w http.ResponseWriter, r *http.Request) { if !bookmarkingEnabled { http.Redirect(w, r, "/", http.StatusSeeOther) @@ -439,25 +523,54 @@ func bookmarkPostHandler(w http.ResponseWriter, r *http.Request) { if next == "" { next = "/" } - // read existing - items := readBookmarksFromReq(r) - // remove if exists then prepend - newItems := make([]string, 0, 32) - newItems = append(newItems, q) - for _, v := range items { - if v == q { + entries := readBookmarksFromReq(r) + new := []BookmarkEntry{{Type: "q", Value: q}} + for _, e := range entries { + if e.Type == "q" && e.Value == q { continue } - newItems = append(newItems, v) - if len(newItems) >= 30 { + new = append(new, e) + if len(new) >= maxBookmarks { break } } - setBookmarksCookie(w, newItems) + setBookmarksCookie(w, new) + http.Redirect(w, r, next, http.StatusSeeOther) +} + +func bookmarkImagePostHandler(w http.ResponseWriter, r *http.Request) { + if !bookmarkingEnabled { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + if err := r.ParseForm(); err != nil { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + u := strings.TrimSpace(r.FormValue("url")) + if u == "" || !(strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://")) { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + next := r.FormValue("next") + if next == "" { + next = "/" + } + entries := readBookmarksFromReq(r) + new := []BookmarkEntry{{Type: "img", Value: u}} + for _, e := range entries { + if e.Type == "img" && e.Value == u { + continue + } + new = append(new, e) + if len(new) >= maxBookmarks { + break + } + } + setBookmarksCookie(w, new) http.Redirect(w, r, next, http.StatusSeeOther) } -// POST /bookmark_remove : remove a bookmark (form q) func bookmarkRemoveHandler(w http.ResponseWriter, r *http.Request) { if !bookmarkingEnabled { http.Redirect(w, r, "/", http.StatusSeeOther) @@ -467,25 +580,125 @@ func bookmarkRemoveHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusSeeOther) return } - q := strings.TrimSpace(r.FormValue("q")) - items := readBookmarksFromReq(r) - if len(items) == 0 { + typ := r.FormValue("type") + val := r.FormValue("value") + if typ == "" || val == "" { http.Redirect(w, r, "/", http.StatusSeeOther) return } - out := make([]string, 0, len(items)) - for _, v := range items { - if v == q { + entries := readBookmarksFromReq(r) + out := make([]BookmarkEntry, 0, len(entries)) + for _, e := range entries { + if e.Type == typ && e.Value == val { continue } - out = append(out, v) + out = append(out, e) + } + if len(out) == 0 { + clearBookmarksCookie(w) + } else { + setBookmarksCookie(w, out) } - // set or clear cookie - removeBookmarkCookie(w, out) http.Redirect(w, r, "/", http.StatusSeeOther) } -// imageProxy uses pooled buffer for io.CopyBuffer +func bookmarksExportHandler(w http.ResponseWriter, r *http.Request) { + if !bookmarkingEnabled { + http.Error(w, "bookmarks disabled", http.StatusNotFound) + return + } + entries := readBookmarksFromReq(r) + if entries == nil { + entries = []BookmarkEntry{} + } + js, err := json.MarshalIndent(entries, "", " ") + if err != nil { + http.Error(w, "failed to export", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Content-Disposition", "attachment; filename=\"pinata_bookmarks.json\"") + _, _ = w.Write(js) +} + +func bookmarksImportHandler(w http.ResponseWriter, r *http.Request) { + if !bookmarkingEnabled { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + r.Body = http.MaxBytesReader(w, r.Body, 2<<20) // 2MB + if err := r.ParseMultipartForm(2 << 20); err != nil { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + file, _, err := r.FormFile("file") + if err != nil { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + defer file.Close() + dec := json.NewDecoder(file) + var entries []BookmarkEntry + if err := dec.Decode(&entries); err == nil { + // ok + } else { + if _, err := file.Seek(0, io.SeekStart); err == nil { + var arr []string + dec2 := json.NewDecoder(file) + if err2 := dec2.Decode(&arr); err2 == nil { + entries = make([]BookmarkEntry, 0, len(arr)) + for _, s := range arr { + entries = append(entries, BookmarkEntry{Type: "q", Value: s}) + } + } else { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + } else { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + } + existing := readBookmarksFromReq(r) + merged := make([]BookmarkEntry, 0, maxBookmarks) + seen := map[string]bool{} + add := func(e BookmarkEntry) { + key := e.Type + "|" + e.Value + if seen[key] { + return + } + seen[key] = true + merged = append(merged, e) + } + for _, e := range entries { + e.Value = strings.TrimSpace(e.Value) + if e.Value == "" { + continue + } + if len(e.Value) > maxItemLen { + e.Value = e.Value[:maxItemLen] + } + if e.Type != "q" && e.Type != "img" { + e.Type = "q" + } + add(e) + if len(merged) >= maxBookmarks { + break + } + } + for _, e := range existing { + add(e) + if len(merged) >= maxBookmarks { + break + } + } + setBookmarksCookie(w, merged) + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +// -------------------------------------------------------------------- +// Image proxy and reverse search +// -------------------------------------------------------------------- func imageProxyHandler(w http.ResponseWriter, r *http.Request) { uq := r.URL.Query().Get("url") if uq == "" { @@ -511,7 +724,6 @@ func imageProxyHandler(w http.ResponseWriter, r *http.Request) { return } defer resp.Body.Close() - if ct := resp.Header.Get("Content-Type"); ct != "" { w.Header().Set("Content-Type", ct) } @@ -519,19 +731,17 @@ func imageProxyHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", cc) } w.WriteHeader(resp.StatusCode) - bufPtr := copyBufPool.Get().(*[]byte) buf := *bufPtr _, _ = io.CopyBuffer(w, resp.Body, buf) copyBufPool.Put(bufPtr) } -// revsearch redirect to tineye func revsearchHandler(w http.ResponseWriter, r *http.Request) { if disableReverse { - http.Error(w, "Reverse image search disabled", http.StatusNotFound) - return - } + http.Error(w, "reverse disabled", http.StatusNotFound) + return + } b64 := r.URL.Query().Get("b64") if b64 == "" { http.Error(w, "b64 required", http.StatusBadRequest) @@ -551,12 +761,9 @@ func revsearchHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, tineye, http.StatusSeeOther) } -// static CSS -func styleHandler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/css; charset=utf-8") - io.WriteString(w, cssContent) -} - +// -------------------------------------------------------------------- +// main: route setup +// -------------------------------------------------------------------- func main() { mux := http.NewServeMux() mux.HandleFunc("/static/style.css", styleHandler) @@ -564,17 +771,22 @@ func main() { mux.HandleFunc("/search", searchHandler) mux.HandleFunc("/image_proxy", imageProxyHandler) mux.HandleFunc("/revsearch", revsearchHandler) - // bookmark endpoints (POST) - mux.HandleFunc("/bookmark", bookmarkPostHandler) - mux.HandleFunc("/bookmark_remove", bookmarkRemoveHandler) - srv := &http.Server{ + // bookmark endpoints + mux.HandleFunc("/bookmark", bookmarkPostHandler) + mux.HandleFunc("/bookmark_image", bookmarkImagePostHandler) + mux.HandleFunc("/bookmark_remove", bookmarkRemoveHandler) + mux.HandleFunc("/bookmarks/export", bookmarksExportHandler) + mux.HandleFunc("/bookmarks/import", bookmarksImportHandler) + + server := &http.Server{ Addr: ":8080", Handler: mux, ReadTimeout: 12 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, } - log.Println("Pinata listening on :8080") - log.Fatal(srv.ListenAndServe()) + + log.Println("Pinata listening on :8080. Bookmarking enabled:", bookmarkingEnabled, " Reverse disabled:", disableReverse) + log.Fatal(server.ListenAndServe()) }