~gitpocket/gitpocket

1
596ffcb7561c765dcb863537f5fce31ae758797e
mainfeed-discovery
gitpocket/attach.go -rw-r--r--3.0 KiB ViewLogBlameView rawPermalink a6b5bf4fe7 feat(markdown): image attachments and preview 8 days ago
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
package main

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"image"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"time"
)

const maxAttachment = 5 << 20

var (
	attachName = regexp.MustCompile(`^[0-9a-f]{64}\.(png|jpg|gif|webp)$`)
	attachExt  = map[string]string{"png": "png", "jpeg": "jpg", "gif": "gif", "webp": "webp"}
	attachType = map[string]string{"png": "image/png", "jpg": "image/jpeg", "gif": "image/gif", "webp": "image/webp"}
)

func (s *Server) upload(w http.ResponseWriter, r *http.Request) {
	u := s.current(r)
	if u == nil {
		http.Error(w, "log in to attach images", http.StatusUnauthorized)
		return
	}
	if s.uploads.blocked(u.Name) {
		http.Error(w, "too many uploads, try again later", http.StatusTooManyRequests)
		return
	}
	data, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxAttachment))
	if err != nil {
		http.Error(w, "images are limited to 5 MB", http.StatusRequestEntityTooLarge)
		return
	}
	cfg, format, err := image.DecodeConfig(bytes.NewReader(data))
	ext, ok := attachExt[format]
	if err != nil || !ok || cfg.Width*cfg.Height > 50_000_000 {
		http.Error(w, "only png, jpeg, gif and webp images can be attached", http.StatusUnsupportedMediaType)
		return
	}
	sum := sha256.Sum256(data)
	name := hex.EncodeToString(sum[:]) + "." + ext
	dir := filepath.Join(s.root, "attachments")
	path := filepath.Join(dir, name)
	if _, err := os.Stat(path); err != nil {
		tmp, err := os.CreateTemp(dir, "upload")
		if os.IsNotExist(err) {
			os.MkdirAll(dir, 0o755)
			tmp, err = os.CreateTemp(dir, "upload")
		}
		if err != nil {
			s.fail(w, r, 500, err.Error())
			return
		}
		_, err = tmp.Write(data)
		if cerr := tmp.Close(); err == nil {
			err = cerr
		}
		if err == nil {
			err = os.Chmod(tmp.Name(), 0o644)
		}
		if err == nil {
			err = os.Rename(tmp.Name(), path)
		}
		if err != nil {
			os.Remove(tmp.Name())
			s.fail(w, r, 500, err.Error())
			return
		}
	}
	s.uploads.fail(u.Name)
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"url": "/attachments/" + name})
}

func (s *Server) attachment(w http.ResponseWriter, r *http.Request) {
	name := r.PathValue("name")
	if !attachName.MatchString(name) {
		http.NotFound(w, r)
		return
	}
	f, err := os.Open(filepath.Join(s.root, "attachments", name))
	if err != nil {
		http.NotFound(w, r)
		return
	}
	defer f.Close()
	w.Header().Set("Content-Type", attachType[filepath.Ext(name)[1:]])
	w.Header().Set("X-Content-Type-Options", "nosniff")
	w.Header().Set("Content-Security-Policy", "default-src 'none'; sandbox")
	w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
	http.ServeContent(w, r, "", time.Time{}, f)
}

func (s *Server) preview(w http.ResponseWriter, r *http.Request) {
	if s.current(r) == nil {
		http.Error(w, "log in to preview", http.StatusUnauthorized)
		return
	}
	r.Body = http.MaxBytesReader(w, r.Body, 256<<10)
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	io.WriteString(w, string(s.renderMarkdown([]byte(r.FormValue("body")))))
}