API: Rig up mutation { create }
This commit is contained in:
parent
fa643487fd
commit
898db2a4eb
|
@ -1,5 +1,28 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/99designs/gqlgen
|
||||
|
||||
type Resolver struct{}
|
||||
|
||||
// via https://github.com/dolmen-go/contextio
|
||||
// Apache 2.0
|
||||
type contextReader struct {
|
||||
ctx context.Context
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func (r *contextReader) Read(p []byte) (n int, err error) {
|
||||
if err := r.ctx.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return r.r.Read(p)
|
||||
}
|
||||
|
||||
func NewContextReader(ctx context.Context, r io.Reader) io.Reader {
|
||||
return &contextReader{ctx, r}
|
||||
}
|
||||
|
|
|
@ -122,8 +122,11 @@ type Query {
|
|||
type Mutation {
|
||||
# Creates a new paste from a list of files. The files uploaded must have a
|
||||
# content type of text/* and must be decodable as UTF-8.
|
||||
#
|
||||
# Note that the web UI will replace CRLF with LF in uploads; the GraphQL API
|
||||
# does not.
|
||||
create(
|
||||
files: [Upload]!,
|
||||
files: [Upload!]!,
|
||||
visibility: Visibility!,
|
||||
): Paste! @access(scope: PASTES, kind: RW)
|
||||
|
||||
|
|
|
@ -5,13 +5,21 @@ package graph
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.sr.ht/~sircmpwn/core-go/auth"
|
||||
"git.sr.ht/~sircmpwn/core-go/config"
|
||||
"git.sr.ht/~sircmpwn/core-go/database"
|
||||
"git.sr.ht/~sircmpwn/core-go/valid"
|
||||
coremodel "git.sr.ht/~sircmpwn/core-go/model"
|
||||
"git.sr.ht/~sircmpwn/paste.sr.ht/api/graph/api"
|
||||
"git.sr.ht/~sircmpwn/paste.sr.ht/api/graph/model"
|
||||
|
@ -43,7 +51,134 @@ func (r *fileResolver) Contents(ctx context.Context, obj *model.File) (*model.UR
|
|||
}
|
||||
|
||||
func (r *mutationResolver) Create(ctx context.Context, files []*graphql.Upload, visibility model.Visibility) (*model.Paste, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
var paste model.Paste
|
||||
valid := valid.New(ctx)
|
||||
nameSet := make(map[string]interface{})
|
||||
for i, file := range files {
|
||||
mt, params, err := mime.ParseMediaType(file.ContentType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
valid.Expect(strings.HasPrefix(mt, "text/"),
|
||||
"File index %d with non-text mimetypes is not acceptable", i)
|
||||
if enc, ok := params["charset"]; ok {
|
||||
valid.Expect(strings.ToLower(enc) == "utf-8",
|
||||
"File index %d with non-UTF-8 encoding is not acceptable", i)
|
||||
}
|
||||
if _, ok := nameSet[file.Filename]; ok {
|
||||
valid.Error("File name '%s' cannot be specified more than once",
|
||||
file.Filename)
|
||||
}
|
||||
nameSet[file.Filename] = nil
|
||||
}
|
||||
if !valid.Ok() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
|
||||
pasteHash := sha1.New()
|
||||
|
||||
row := tx.QueryRowContext(ctx, `
|
||||
INSERT INTO paste (
|
||||
created, updated, user_id, visibility
|
||||
) VALUES (
|
||||
NOW() at time zone 'utc',
|
||||
NOW() at time zone 'utc',
|
||||
$1, $2
|
||||
) RETURNING id;`,
|
||||
auth.ForContext(ctx).UserID,
|
||||
strings.ToLower(visibility.String()))
|
||||
|
||||
var pasteId int
|
||||
if err := row.Scan(&pasteId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, file := range files {
|
||||
fileHash := sha1.New()
|
||||
reader := NewContextReader(ctx, io.TeeReader(file.File, fileHash))
|
||||
bytes, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
valid.Expect(utf8.Valid(bytes),
|
||||
"File index %d has unacceptable non-UTF-8 data", i)
|
||||
if !valid.Ok() {
|
||||
// valid.Expect handles errors for us but we need to return a
|
||||
// non-nil error to WithTx so that the transaction is aborted.
|
||||
return fmt.Errorf("Invalid file input")
|
||||
}
|
||||
content := string(bytes)
|
||||
hash := fileHash.Sum(nil)
|
||||
checksum := hex.EncodeToString(hash[:])
|
||||
|
||||
row := tx.QueryRowContext(ctx, `
|
||||
INSERT INTO blob (
|
||||
created, updated, sha, contents
|
||||
) VALUES (
|
||||
NOW() at time zone 'utc',
|
||||
NOW() at time zone 'utc',
|
||||
$1, $2
|
||||
)
|
||||
ON CONFLICT ON CONSTRAINT sha_unique
|
||||
DO UPDATE SET updated = NOW() at time zone 'utc'
|
||||
RETURNING id;
|
||||
`, checksum, content)
|
||||
|
||||
var blobId int
|
||||
if err := row.Scan(&blobId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var filename *string
|
||||
if file.Filename == "" {
|
||||
pasteHash.Write([]byte{0})
|
||||
filename = nil
|
||||
} else {
|
||||
fmt.Fprintf(pasteHash, "%s", file.Filename)
|
||||
filename = &file.Filename
|
||||
}
|
||||
fmt.Fprintf(pasteHash, "%s", checksum)
|
||||
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO paste_file (
|
||||
created, updated, filename, blob_id, paste_id
|
||||
) VALUES (
|
||||
NOW() at time zone 'utc',
|
||||
NOW() at time zone 'utc',
|
||||
$1, $2, $3
|
||||
);`,
|
||||
filename, blobId, pasteId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(pasteHash, "%d", auth.ForContext(ctx).UserID)
|
||||
fmt.Fprintf(pasteHash, "%d", pasteId)
|
||||
|
||||
hash := pasteHash.Sum(nil)
|
||||
checksum := hex.EncodeToString(hash[:])
|
||||
_, err := tx.ExecContext(ctx,
|
||||
`UPDATE paste SET sha = $1 WHERE id = $2`,
|
||||
checksum, pasteId)
|
||||
|
||||
paste.ID = checksum
|
||||
paste.PKID = pasteId
|
||||
paste.Created = time.Now().UTC()
|
||||
paste.UserID = auth.ForContext(ctx).UserID
|
||||
paste.RawVisibility = strings.ToLower(visibility.String())
|
||||
return err
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !valid.Ok() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// TODO: Schedule webhooks
|
||||
return &paste, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) Update(ctx context.Context, id string, visibility model.Visibility) (*model.Paste, error) {
|
||||
|
|
Loading…
Reference in New Issue