package artifact import ( "archive/tar" "crypto/sha256" "encoding/base64" "errors" "fmt" "io" "net/http" "os" "path/filepath" "strconv" "strings" "go.sour.is/paste/src/pkg/readutil" "golang.org/x/sys/unix" "sour.is/x/toolbox/httpsrv" "sour.is/x/toolbox/log" "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/html" "github.com/gomarkdown/markdown/parser" ) // artifact stores items to disk type artifact struct { store string maxSize int64 } const DefaultMaxSize = 500 * 1024 * 1024 func New(store string, maxSize int64) (a *artifact, err error) { a = &artifact{ store: store, maxSize: DefaultMaxSize, } if maxSize > 0 { a.maxSize = maxSize } if !chkStore(a.store) { return nil, fmt.Errorf("artifact Store location [%s] does not exist or is not writable", a.store) } return a, nil } func (a *artifact) RegisterHTTP(mux *http.ServeMux) { mux.Handle("/a", http.StripPrefix("/a", a)) mux.Handle("/a/", http.StripPrefix("/a/", a)) } func (a *artifact) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: if r.URL.Path == "" { a.list(w) return } name := strings.TrimPrefix(r.URL.Path, "/") name, path, _ := strings.Cut(name, "/") a.get(w, name, path) case http.MethodPost: length := 0 if h := r.Header.Get("Content-Length"); h != "" { if i, err := strconv.Atoi(h); err != nil { length = i } } id, err := a.put(r.Body, length) switch { case errors.Is(err, ErrGone): w.WriteHeader(http.StatusGone) case errors.Is(err, ErrNotFound): w.WriteHeader(http.StatusNotFound) case errors.Is(err, ErrReadingContent): w.WriteHeader(http.StatusInternalServerError) } fmt.Fprintf(w, "OK %s", id) default: http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) } } func (a *artifact) get(w http.ResponseWriter, name string, path string) error { hasPath := path != "" ext := filepath.Ext(name) id := strings.TrimSuffix(name, ext) fname := filepath.Join(a.store, id) if !chkFile(fname) { return fmt.Errorf("%w: %s", ErrNotFound, fname) } if chkGone(fname) { return fmt.Errorf("%w: %s", ErrGone, fname) } f, err := os.Open(fname) if err != nil { return err } defer f.Close() if !hasPath { pr := readutil.NewPreviewReader(f) mime, err := readutil.ReadMIME(pr, name) if err != nil { return err } w.Header().Set("Content-Type", mime) w.Header().Set("X-Content-Type-Options", "nosniff") _, _ = io.Copy(w, pr.Drain()) return nil } rdr, err := readutil.Decompress(f) if err != nil && err != readutil.ErrUnsupportedType { return fmt.Errorf("%w: %w", ErrReadingContent, err) } if cl, ok := rdr.(io.ReadCloser); ok { defer cl.Close() } tr := tar.NewReader(rdr) if path == "@" { var paths []string for { hdr, err := tr.Next() if err == io.EOF { break // End of archive } if err != nil { log.Error(err) } paths = append(paths, hdr.Name) } httpsrv.WriteObject(w, http.StatusOK, paths) return nil } for { hdr, err := tr.Next() if err == io.EOF { break // End of archive } if err != nil { log.Error(err) break } if path == "~" && hdr.Name == "index.html" { path = hdr.Name } if path == "~" && hdr.Name == "index.md" { path = hdr.Name } if hdr.Name == path { if strings.HasSuffix(hdr.Name, ".md") { md, err := io.ReadAll(tr) if err != nil { return fmt.Errorf("%w: %w", ErrReadingContent, err) } w.Header().Set("Content-Type", "text/html") extensions := parser.CommonExtensions | parser.AutoHeadingIDs p := parser.NewWithExtensions(extensions) htmlFlags := html.CommonFlags | html.HrefTargetBlank opts := html.RendererOptions{Flags: htmlFlags} renderer := html.NewRenderer(opts) b := markdown.ToHTML(md, p, renderer) _, _ = w.Write(b) return nil } pr := readutil.NewPreviewReader(tr) mime, err := readutil.ReadMIME(pr, hdr.Name) if err != nil { return fmt.Errorf("%w: %w", ErrReadingContent, err) } w.Header().Set("Content-Type", mime) if _, err := io.Copy(w, pr.Drain()); err != nil { log.Error(err) } return nil } } http.Error(w, "Not Found in Archive", http.StatusNotFound) return ErrNotFound } func (a *artifact) put(r io.ReadCloser, length int) (string, error) { defer r.Close() if length > 0 { if int64(length) > a.maxSize { return "", ErrSizeTooLarge } } rdr := io.LimitReader(r, a.maxSize) pr := readutil.NewPreviewReader(rdr) rdr = pr.Drain() s256 := sha256.New() tmp, err := os.CreateTemp(a.store, "arch-") if err != nil { return "", fmt.Errorf("%w: %w", ErrBadInput, err) } defer os.Remove(tmp.Name()) m := io.MultiWriter(s256, tmp) if _, err := io.Copy(m, rdr); err != nil { return "", fmt.Errorf("%w: %w", ErrBadInput, err) } tmp.Close() id := base64.RawURLEncoding.EncodeToString(s256.Sum(nil)[12:]) fname := filepath.Join(a.store, id) // log.Debugs("Artifact: moving file", "src", tmp.Name(), "dst", fname) _ = os.Rename(tmp.Name(), fname) return id, nil } func (a *artifact) list(w io.Writer) error { return filepath.Walk(a.store, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } _, err = fmt.Fprintln(w, "FILE: ", info.Name()) return err }) } func chkStore(path string) bool { file, err := os.Stat(path) if err != nil && os.IsNotExist(err) { err = os.MkdirAll(path, 0744) if err != nil { return false } file, err = os.Stat(path) } if err != nil { return false } if !file.IsDir() { return false } if unix.Access(path, unix.W_OK&unix.R_OK) != nil { return false } return true } func chkFile(path string) bool { file, err := os.Stat(path) if err != nil { return false } if file.IsDir() { return false } if unix.Access(path, unix.W_OK&unix.R_OK) != nil { return false } return true } func chkGone(path string) bool { file, err := os.Stat(path) if err != nil { return true } if file.Size() == 0 { return true } return false } var ( ErrNotFound = errors.New("not found") ErrGone = errors.New("gone") ErrReadingContent = errors.New("reading content") ErrSizeTooLarge = errors.New("size too large") ErrBadInput = errors.New("bad input") )