package export import ( "context" "errors" "fmt" "io" "log" "net/http" "os" "path" "time" "git.sr.ht/~emersion/gqlclient" "git.sr.ht/~xenrox/hut/srht/listssrht" ) const archiveFilename = "archive.mbox" type ListsExporter struct { client *gqlclient.Client http *http.Client } func NewListsExporter(client *gqlclient.Client, http *http.Client) *ListsExporter { newHttp := *http // XXX: Is this a sane default? Maybe large lists or slow // connections could require more. Would be nice to ensure a // constant flow of data rather than ensuring the entire request is // complete within a deadline. Would also be nice to use range // headers to be able to resume this on failure or interruption. newHttp.Timeout = 10 * time.Minute return &ListsExporter{ client: client, http: &newHttp, } } // A subset of listssrht.MailingList which only contains the fields we want to // export (i.e. the ones filled in by the GraphQL query) type MailingListInfo struct { Info Description *string `json:"description"` Visibility listssrht.Visibility `json:"visibility"` PermitMime []string `json:"permitMime"` RejectMime []string `json:"rejectMime"` } func (ex *ListsExporter) Export(ctx context.Context, dir string) error { var cursor *listssrht.Cursor var ret error for { user, err := listssrht.ExportMailingLists(ex.client, ctx, cursor) if err != nil { return err } for _, list := range user.Lists.Results { base := path.Join(dir, list.Name) if err := os.MkdirAll(base, 0o755); err != nil { return err } if err := ex.exportList(ctx, &list, base); err != nil { var pe partialError if errors.As(err, &pe) { ret = err continue } return err } } cursor = user.Lists.Cursor if cursor == nil { break } } return ret } func (ex *ListsExporter) ExportResource(ctx context.Context, dir, owner, resource string) error { user, err := listssrht.ExportMailingList(ex.client, ctx, owner, resource) if err != nil { return err } return ex.exportList(ctx, user.List, dir) } func (ex *ListsExporter) exportList(ctx context.Context, list *listssrht.MailingList, base string) error { infoPath := path.Join(base, infoFilename) if _, err := os.Stat(infoPath); err == nil { log.Printf("\tSkipping %s (already exists)", list.Name) return nil } log.Printf("\t%s", list.Name) req, err := http.NewRequestWithContext(ctx, http.MethodGet, string(list.Archive), nil) if err != nil { return err } resp, err := ex.http.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return partialError{fmt.Errorf("%s: server returned non-200 status %d", list.Name, resp.StatusCode)} } archive, err := os.Create(path.Join(base, archiveFilename)) if err != nil { return err } defer archive.Close() if _, err := io.Copy(archive, resp.Body); err != nil { return err } listInfo := MailingListInfo{ Info: Info{ Service: "lists.sr.ht", Name: list.Name, }, Description: list.Description, Visibility: list.Visibility, PermitMime: list.PermitMime, RejectMime: list.RejectMime, } if err := writeJSON(infoPath, &listInfo); err != nil { return err } return nil } func (ex *ListsExporter) ImportResource(ctx context.Context, dir string) error { var info MailingListInfo if err := readJSON(path.Join(dir, infoFilename), &info); err != nil { return err } return ex.importList(ctx, &info, dir) } func (ex *ListsExporter) importList(ctx context.Context, list *MailingListInfo, base string) error { l, err := listssrht.CreateMailingList(ex.client, ctx, list.Name, list.Description, list.Visibility) if err != nil { return fmt.Errorf("failed to create mailing list: %v", err) } if _, err := listssrht.UpdateMailingList(ex.client, ctx, l.Id, listssrht.MailingListInput{ PermitMime: list.PermitMime, RejectMime: list.RejectMime, }); err != nil { return fmt.Errorf("failed to update mailing list: %v", err) } archive, err := os.Open(path.Join(base, archiveFilename)) if err != nil { return err } defer archive.Close() if _, err := listssrht.ImportMailingListSpool(ex.client, ctx, l.Id, gqlclient.Upload{ Filename: archiveFilename, MIMEType: "application/mbox", Body: archive, }); err != nil { return fmt.Errorf("failed to import mailing list emails: %v", err) } return nil }