diff --git a/appview/db/migration.go b/appview/db/migration.go new file mode 100644 index 00000000..0b69d0c8 --- /dev/null +++ b/appview/db/migration.go @@ -0,0 +1,18 @@ +package db + +import ( + "context" + + "github.com/bluesky-social/indigo/atproto/syntax" + "tangled.org/core/appview/models" +) + +// "migration" for records stored in user's PDS, not AppView DB + +// NOTE: each migration row should represent atomic operation. + +func ListPDSRecordMigrations(ctx context.Context, e Execer, user syntax.DID) ([]*models.PDSRecordMigration, error) { + // select did, name, record from pds_migrations group by (did, name) + // merge by (did, name) + panic("unimplemented") +} diff --git a/appview/migration/migration.go b/appview/migration/migration.go new file mode 100644 index 00000000..768e8a98 --- /dev/null +++ b/appview/migration/migration.go @@ -0,0 +1,154 @@ +package migration + +import ( + "context" + "fmt" + "html" + "net/http" + + comatproto "github.com/bluesky-social/indigo/api/atproto" + "github.com/bluesky-social/indigo/atproto/atclient" + "github.com/bluesky-social/indigo/atproto/syntax" + lexutil "github.com/bluesky-social/indigo/lex/util" + "tangled.org/core/api/tangled" + "tangled.org/core/appview/db" + "tangled.org/core/appview/oauth" + "tangled.org/core/appview/pages" +) + +type Migration struct { + db *db.DB + oauth *oauth.OAuth + pages *pages.Pages +} + +// migration page handler +func (s *Migration) HandleMigration(w http.ResponseWriter, r *http.Request) { + returnURL := r.URL.Query().Get("return_url") + if returnURL == "" { + returnURL = "/" + } + + panic("unimplemented") +} + +// HandleMigrationProgress is migration progress handler +// This is the place where migration process gets started. +func (s *Migration) HandleMigrationProgress(w http.ResponseWriter, r *http.Request) { + user := s.oauth.GetMultiAccountUser(r) + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + panic("unimplemented") + } + + flusher, ok := w.(http.Flusher) + if !ok { + panic("unimplemented") + } + + // start SSE streaming + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("X-Accel-Buffering", "no") + + send := func(event, data string) { + fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, data) + flusher.Flush() + } + sendStepProgress := func(stepName string, curr, max int) { + send("StepProgress", fmt.Sprintf(`[%d/%d]: %s`, curr, max, html.EscapeString(stepName))) + } + sendRecordProgress := func(curr, max int) { + percent := 100 * curr / max + send("RecordProgress", fmt.Sprintf(` %d%%`, curr, max, percent)) + } + sendError := func(msg string) { + send("Error", fmt.Sprintf(`%s`, html.EscapeString(msg))) + } + + // event:StepProgress ->
[1/3]: adding repository name to migration
+ // event:RecordProgress -> + // event:Done -> "" + + //
+ // or just use js. whatever. just need to handle the event from client side. + + ctx := r.Context() + + migrations, err := db.ListPDSRecordMigrations(ctx, s.db, syntax.DID(user.Active.Did)) + if err != nil { + // 500 maybe? + panic("unimplemented") + } + + for i, migration := range migrations { + var message string + var migrateFn func(context.Context, *atclient.APIClient, syntax.DID, syntax.ATURI) error + switch migration.Name { + case "add-repo-did": + message = `adding repoDid field for all repo-pointing records` + migrateFn = s.migrateAddRepoDid + case "use-feed-comment": + message = `rewriting existing comments to sh.tangled.feed.comment lexicon` + migrateFn = s.migrateUseFeedComment + default: + continue + } + sendStepProgress(message, i, len(migrations)) + + for j, record := range migration.Records { + if err := migrateFn(ctx, client, migration.Did, record); err != nil { + panic("unimplemented") + } + sendRecordProgress(j+1, len(migration.Records)) + } + } + + send("Done", "") +} + +func (s *Migration) migrateAddRepoDid(ctx context.Context, client *atclient.APIClient, did syntax.DID, record syntax.ATURI) error { + var ( + collection = record.Collection() + rkey = record.RecordKey() + ) + ex, err := comatproto.RepoGetRecord(ctx, client, "", collection.String(), did.String(), rkey.String()) + if err != nil { + // record doesn't exist, should we fail? + panic("unimplemented") + } + + // uhhh if it's legacy record, there are pretty high possibility that we lost some data + // while unmarshalling it. + // so we should not depend on cbor unmarshaller. + val := ex.Value.Val + + switch record.Collection() { + case tangled.RepoNSID: + case tangled.RepoIssueNSID: + case tangled.RepoPullNSID: + case tangled.RepoCollaboratorNSID: + case tangled.RepoArtifactNSID: + case tangled.FeedStarNSID: + case tangled.ActorProfileNSID: + default: + return fmt.Errorf("unexpected collection: '%s'", record.Collection()) + } + + _, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ + Collection: collection.String(), + Repo: did.String(), + Rkey: rkey.String(), + SwapRecord: ex.Cid, + Record: &lexutil.LexiconTypeDecoder{Val: val}, + }) + if err != nil { + return fmt.Errorf("put record: %w", err) + } + + return nil +} + +func (s *Migration) migrateUseFeedComment(ctx context.Context, client *atclient.APIClient, did syntax.DID, record syntax.ATURI) error { + panic("unimplemented") +} diff --git a/appview/models/migration.go b/appview/models/migration.go new file mode 100644 index 00000000..ccba61ba --- /dev/null +++ b/appview/models/migration.go @@ -0,0 +1,11 @@ +package models + +import ( + "github.com/bluesky-social/indigo/atproto/syntax" +) + +type PDSRecordMigration struct { + Did syntax.DID + Name string // name of the migration + Records []syntax.ATURI // records that need a migration +} diff --git a/flake.nix b/flake.nix index 9e66decc..bedbd16e 100644 --- a/flake.nix +++ b/flake.nix @@ -29,6 +29,10 @@ url = "https://cdn.jsdelivr.net/npm/htmx-ext-ws@2.0.2"; flake = false; }; + htmx-sse-src = { + url = "https://cdn.jsdelivr.net/npm/htmx-ext-sse@2.2.4"; + flake = false; + }; lucide-src = { url = "https://github.com/lucide-icons/lucide/releases/download/0.536.0/lucide-icons-0.536.0.zip"; flake = false;