API: Rig up mutation { create }

This commit is contained in:
Drew DeVault 2021-09-21 14:28:11 +02:00
parent fa643487fd
commit 898db2a4eb
3 changed files with 163 additions and 2 deletions

View File

@ -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}
}

View File

@ -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)

View File

@ -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) {