package export import ( "context" "errors" "fmt" "io" "log" "net/http" "os" "path" "time" "git.sr.ht/~emersion/gqlclient" "git.sr.ht/~xenrox/hut/srht/pastesrht") const pasteFilesDir = "files" type PasteExporter struct { client *gqlclient.Client http *http.Client} func NewPasteExporter(client *gqlclient.Client, http *http.Client) *PasteExporter { // XXX: Is this a sane default? newHttp := *http newHttp.Timeout = 10 * time.Minute return &PasteExporter{ client: client, http: &newHttp, }} type PasteInfo struct { Info Visibility pastesrht.Visibility `json:"visibility"`} func (ex *PasteExporter) Export(ctx context.Context, dir string) error { var cursor *pastesrht.Cursor var ret error for { pastes, err := pastesrht.PasteContents(ex.client, ctx, cursor) if err != nil { return err } for _, paste := range pastes.Results { base := path.Join(dir, paste.Id) if err := ex.exportPaste(ctx, &paste, base); err != nil { var pe partialError if errors.As(err, &pe) { ret = err continue } return err } } cursor = pastes.Cursor if cursor == nil { break } } return ret} func (ex *PasteExporter) ExportResource(ctx context.Context, dir, owner, resource string) error { paste, err := pastesrht.PasteContentsByID(ex.client, ctx, resource) if err != nil { return err } return ex.exportPaste(ctx, paste, dir)} func (ex *PasteExporter) exportPaste(ctx context.Context, paste *pastesrht.Paste, base string) error { infoPath := path.Join(base, infoFilename) if _, err := os.Stat(infoPath); err == nil { log.Printf("\tSkipping %s (already exists)", paste.Id) return nil } log.Printf("\t%s", paste.Id) files := path.Join(base, pasteFilesDir) if err := os.MkdirAll(files, 0o755); err != nil { return err } var ret error for _, file := range paste.Files { if err := ex.exportFile(ctx, paste, files, &file); err != nil { ret = err } } pasteInfo := PasteInfo{ Info: Info{ Service: "paste.sr.ht", Name: paste.Id, }, Visibility: paste.Visibility, } if err := writeJSON(infoPath, &pasteInfo); err != nil { return err } return ret} func (ex *PasteExporter) exportFile(ctx context.Context, paste *pastesrht.Paste, base string, file *pastesrht.File) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, string(file.Contents), nil) if err != nil { return err } resp, err := ex.http.Do(req) if err != nil { return err } defer resp.Body.Close() name := paste.Id if file.Filename != nil && *file.Filename != "" { name = *file.Filename } if resp.StatusCode != http.StatusOK { return partialError{fmt.Errorf("%s/%s: server returned non-200 status %d", paste.Id, name, resp.StatusCode)} } f, err := os.Create(path.Join(base, name)) if err != nil { return err } defer f.Close() _, err = io.Copy(f, resp.Body) if err != nil { return err } return nil} func (ex *PasteExporter) ImportResource(ctx context.Context, dir string) error { var info PasteInfo if err := readJSON(path.Join(dir, infoFilename), &info); err != nil { return err } return ex.importPaste(ctx, &info, dir)} func (ex *PasteExporter) importPaste(ctx context.Context, paste *PasteInfo, base string) error { filesPath := path.Join(base, pasteFilesDir) items, err := os.ReadDir(filesPath) if err != nil { return err } var files []gqlclient.Upload for _, item := range items { if item.IsDir() { continue } f, err := os.Open(path.Join(filesPath, item.Name())) if err != nil { return err } defer f.Close() var name string if item.Name() != paste.Name { name = item.Name() } files = append(files, gqlclient.Upload{ Filename: name, // MIMEType is not used by the API, except for checking that it is a "text". // Parsing the MIME type from the extension would cause issues: ".json" is parsed as "application/json", // which gets rejected because it is not a "text/". // Since the API does not use the type besides that, always send a dummy text value. MIMEType: "text/plain", Body: f, }) } if _, err := pastesrht.CreatePaste(ex.client, ctx, files, paste.Visibility); err != nil { return fmt.Errorf("failed to create paste: %v", err) } return nil}
package export
import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path"
"time"
"git.sr.ht/~emersion/gqlclient"
"git.sr.ht/~xenrox/hut/srht/pastesrht"
)
const pasteFilesDir = "files"
type PasteExporter struct {
client *gqlclient.Client
http *http.Client
}
func NewPasteExporter(client *gqlclient.Client, http *http.Client) *PasteExporter {
// XXX: Is this a sane default?
newHttp := *http
newHttp.Timeout = 10 * time.Minute
return &PasteExporter{
client: client,
http: &newHttp,
}
}
type PasteInfo struct {
Info
Visibility pastesrht.Visibility `json:"visibility"`
}
func (ex *PasteExporter) Export(ctx context.Context, dir string) error {
var cursor *pastesrht.Cursor
var ret error
for {
pastes, err := pastesrht.PasteContents(ex.client, ctx, cursor)
if err != nil {
return err
}
for _, paste := range pastes.Results {
base := path.Join(dir, paste.Id)
if err := ex.exportPaste(ctx, &paste, base); err != nil {
var pe partialError
if errors.As(err, &pe) {
ret = err
continue
}
return err
}
}
cursor = pastes.Cursor
if cursor == nil {
break
}
}
return ret
}
func (ex *PasteExporter) ExportResource(ctx context.Context, dir, owner, resource string) error {
paste, err := pastesrht.PasteContentsByID(ex.client, ctx, resource)
if err != nil {
return err
}
return ex.exportPaste(ctx, paste, dir)
}
func (ex *PasteExporter) exportPaste(ctx context.Context, paste *pastesrht.Paste, base string) error {
infoPath := path.Join(base, infoFilename)
if _, err := os.Stat(infoPath); err == nil {
log.Printf("\tSkipping %s (already exists)", paste.Id)
return nil
}
log.Printf("\t%s", paste.Id)
files := path.Join(base, pasteFilesDir)
if err := os.MkdirAll(files, 0o755); err != nil {
return err
}
var ret error
for _, file := range paste.Files {
if err := ex.exportFile(ctx, paste, files, &file); err != nil {
ret = err
}
}
pasteInfo := PasteInfo{
Info: Info{
Service: "paste.sr.ht",
Name: paste.Id,
},
Visibility: paste.Visibility,
}
if err := writeJSON(infoPath, &pasteInfo); err != nil {
return err
}
return ret
}
func (ex *PasteExporter) exportFile(ctx context.Context, paste *pastesrht.Paste, base string, file *pastesrht.File) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, string(file.Contents), nil)
if err != nil {
return err
}
resp, err := ex.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
name := paste.Id
if file.Filename != nil && *file.Filename != "" {
name = *file.Filename
}
if resp.StatusCode != http.StatusOK {
return partialError{fmt.Errorf("%s/%s: server returned non-200 status %d", paste.Id, name, resp.StatusCode)}
}
f, err := os.Create(path.Join(base, name))
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
if err != nil {
return err
}
return nil
}
func (ex *PasteExporter) ImportResource(ctx context.Context, dir string) error {
var info PasteInfo
if err := readJSON(path.Join(dir, infoFilename), &info); err != nil {
return err
}
return ex.importPaste(ctx, &info, dir)
}
func (ex *PasteExporter) importPaste(ctx context.Context, paste *PasteInfo, base string) error {
filesPath := path.Join(base, pasteFilesDir)
items, err := os.ReadDir(filesPath)
if err != nil {
return err
}
var files []gqlclient.Upload
for _, item := range items {
if item.IsDir() {
continue
}
f, err := os.Open(path.Join(filesPath, item.Name()))
if err != nil {
return err
}
defer f.Close()
var name string
if item.Name() != paste.Name {
name = item.Name()
}
files = append(files, gqlclient.Upload{
Filename: name,
// MIMEType is not used by the API, except for checking that it is a "text".
// Parsing the MIME type from the extension would cause issues: ".json" is parsed as "application/json",
// which gets rejected because it is not a "text/".
// Since the API does not use the type besides that, always send a dummy text value.
MIMEType: "text/plain",
Body: f,
})
}
if _, err := pastesrht.CreatePaste(ex.client, ctx, files, paste.Visibility); err != nil {
return fmt.Errorf("failed to create paste: %v", err)
}
return nil
}