feat: create v2 for module support
All checks were successful
Deploy / deploy (push) Successful in 1m21s
All checks were successful
Deploy / deploy (push) Successful in 1m21s
This commit is contained in:
328
v2/artifact/artifact.go
Normal file
328
v2/artifact/artifact.go
Normal file
@@ -0,0 +1,328 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// func init() {
|
||||
// a := &Artifact{}
|
||||
// httpsrv.RegisterModule("artifact", a.config)
|
||||
|
||||
// httpsrv.HttpRegister("artifact", httpsrv.HttpRoutes{
|
||||
// {Name: "get-path", Method: "GET", Pattern: "/a/{name}/{path:.*}", HandlerFunc: a.get},
|
||||
// {Name: "get", Method: "GET", Pattern: "/a/{name}", HandlerFunc: a.get},
|
||||
// {Name: "put", Method: "PUT", Pattern: "/a", HandlerFunc: a.put},
|
||||
// {Name: "get", Method: "GET", Pattern: "/a", HandlerFunc: a.list},
|
||||
// })
|
||||
// }
|
||||
|
||||
// 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")
|
||||
)
|
||||
Reference in New Issue
Block a user