From 3bb028cc46401a8a54ecab7e7c035dbb24937b6c Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Wed, 26 Jan 2022 09:45:51 +0000 Subject: [PATCH] Validate migration files (#18203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JSON Schema validation for data used by Gitea during migrations Discussion at https://forum.forgefriends.org/t/common-json-schema-for-repository-information/563 Co-authored-by: Loïc Dachary --- .gitignore | 2 + cmd/restore_repo.go | 5 + go.mod | 1 + go.sum | 2 + integrations/dump_restore_test.go | 2 +- modules/migration/file_format.go | 112 +++++++++++++++++ modules/migration/file_format_test.go | 39 ++++++ .../file_format_testdata/issue_a.json | 14 +++ .../file_format_testdata/issue_a.yml | 10 ++ .../file_format_testdata/issue_b.json | 5 + .../file_format_testdata/milestones.json | 20 +++ modules/migration/issue.go | 32 ++--- modules/migration/label.go | 6 +- modules/migration/milestone.go | 14 +-- modules/migration/reaction.go | 6 +- modules/migration/schemas/issue.json | 114 ++++++++++++++++++ modules/migration/schemas/label.json | 28 +++++ modules/migration/schemas/milestone.json | 67 ++++++++++ modules/migration/schemas/reaction.json | 29 +++++ modules/migration/schemas_bindata.go | 10 ++ modules/migration/schemas_dynamic.go | 40 ++++++ modules/migration/schemas_static.go | 17 +++ modules/private/restore_repo.go | 20 +-- routers/private/restore_repo.go | 10 +- services/migrations/dump.go | 4 +- services/migrations/restore.go | 42 ++----- 26 files changed, 577 insertions(+), 74 deletions(-) create mode 100644 modules/migration/file_format.go create mode 100644 modules/migration/file_format_test.go create mode 100644 modules/migration/file_format_testdata/issue_a.json create mode 100644 modules/migration/file_format_testdata/issue_a.yml create mode 100644 modules/migration/file_format_testdata/issue_b.json create mode 100644 modules/migration/file_format_testdata/milestones.json create mode 100644 modules/migration/schemas/issue.json create mode 100644 modules/migration/schemas/label.json create mode 100644 modules/migration/schemas/milestone.json create mode 100644 modules/migration/schemas/reaction.json create mode 100644 modules/migration/schemas_bindata.go create mode 100644 modules/migration/schemas_dynamic.go create mode 100644 modules/migration/schemas_static.go diff --git a/.gitignore b/.gitignore index 98975fab49..eab92b49ad 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,8 @@ _testmain.go coverage.all cpu.out +/modules/migration/bindata.go +/modules/migration/bindata.go.hash /modules/options/bindata.go /modules/options/bindata.go.hash /modules/public/bindata.go diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go index 357bd92c77..f0b01e7984 100644 --- a/cmd/restore_repo.go +++ b/cmd/restore_repo.go @@ -43,6 +43,10 @@ var CmdRestoreRepository = cli.Command{ Usage: `Which items will be restored, one or more units should be separated as comma. wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`, }, + cli.BoolFlag{ + Name: "validation", + Usage: "Sanity check the content of the files before trying to load them", + }, }, } @@ -58,6 +62,7 @@ func runRestoreRepository(c *cli.Context) error { c.String("owner_name"), c.String("repo_name"), c.StringSlice("units"), + c.Bool("validation"), ) if statusCode == http.StatusOK { return nil diff --git a/go.mod b/go.mod index 7266acb751..9ca1429d11 100644 --- a/go.mod +++ b/go.mod @@ -97,6 +97,7 @@ require ( github.com/quasoft/websspi v1.0.0 github.com/rs/xid v1.3.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect github.com/sergi/go-diff v1.2.0 github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 diff --git a/go.sum b/go.sum index 898a73dd88..f632614fbc 100644 --- a/go.sum +++ b/go.sum @@ -1039,6 +1039,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE= +github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= diff --git a/integrations/dump_restore_test.go b/integrations/dump_restore_test.go index 053ca47855..c0e583293c 100644 --- a/integrations/dump_restore_test.go +++ b/integrations/dump_restore_test.go @@ -81,7 +81,7 @@ func TestDumpRestore(t *testing.T) { // newreponame := "restoredrepo" - err = migrations.RestoreRepository(ctx, d, repo.OwnerName, newreponame, []string{"labels", "milestones", "issues", "comments"}) + err = migrations.RestoreRepository(ctx, d, repo.OwnerName, newreponame, []string{"labels", "milestones", "issues", "comments"}, false) assert.NoError(t, err) newrepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: newreponame}).(*repo_model.Repository) diff --git a/modules/migration/file_format.go b/modules/migration/file_format.go new file mode 100644 index 0000000000..30e1d256cd --- /dev/null +++ b/modules/migration/file_format.go @@ -0,0 +1,112 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migration + +import ( + "fmt" + "os" + "strings" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + + "github.com/santhosh-tekuri/jsonschema/v5" + "gopkg.in/yaml.v2" +) + +// Load project data from file, with optional validation +func Load(filename string, data interface{}, validation bool) error { + isJSON := strings.HasSuffix(filename, ".json") + + bs, err := os.ReadFile(filename) + if err != nil { + return err + } + + if validation { + err := validate(bs, data, isJSON) + if err != nil { + return err + } + } + return unmarshal(bs, data, isJSON) +} + +func unmarshal(bs []byte, data interface{}, isJSON bool) error { + if isJSON { + return json.Unmarshal(bs, data) + } + return yaml.Unmarshal(bs, data) +} + +func getSchema(filename string) (*jsonschema.Schema, error) { + c := jsonschema.NewCompiler() + c.LoadURL = openSchema + return c.Compile(filename) +} + +func validate(bs []byte, datatype interface{}, isJSON bool) error { + var v interface{} + err := unmarshal(bs, &v, isJSON) + if err != nil { + return err + } + if !isJSON { + v, err = toStringKeys(v) + if err != nil { + return err + } + } + + var schemaFilename string + switch datatype := datatype.(type) { + case *[]*Issue: + schemaFilename = "issue.json" + case *[]*Milestone: + schemaFilename = "milestone.json" + default: + return fmt.Errorf("file_format:validate: %T has not a validation implemented", datatype) + } + + sch, err := getSchema(schemaFilename) + if err != nil { + return err + } + err = sch.Validate(v) + if err != nil { + log.Error("migration validation with %s failed for\n%s", schemaFilename, string(bs)) + } + return err +} + +func toStringKeys(val interface{}) (interface{}, error) { + var err error + switch val := val.(type) { + case map[interface{}]interface{}: + m := make(map[string]interface{}) + for k, v := range val { + k, ok := k.(string) + if !ok { + return nil, fmt.Errorf("found non-string key %T %s", k, k) + } + m[k], err = toStringKeys(v) + if err != nil { + return nil, err + } + } + return m, nil + case []interface{}: + l := make([]interface{}, len(val)) + for i, v := range val { + l[i], err = toStringKeys(v) + if err != nil { + return nil, err + } + } + return l, nil + default: + return val, nil + } +} diff --git a/modules/migration/file_format_test.go b/modules/migration/file_format_test.go new file mode 100644 index 0000000000..27104e209c --- /dev/null +++ b/modules/migration/file_format_test.go @@ -0,0 +1,39 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migration + +import ( + "strings" + "testing" + + "github.com/santhosh-tekuri/jsonschema/v5" + "github.com/stretchr/testify/assert" +) + +func TestMigrationJSON_IssueOK(t *testing.T) { + issues := make([]*Issue, 0, 10) + err := Load("file_format_testdata/issue_a.json", &issues, true) + assert.NoError(t, err) + err = Load("file_format_testdata/issue_a.yml", &issues, true) + assert.NoError(t, err) +} + +func TestMigrationJSON_IssueFail(t *testing.T) { + issues := make([]*Issue, 0, 10) + err := Load("file_format_testdata/issue_b.json", &issues, true) + if _, ok := err.(*jsonschema.ValidationError); ok { + errors := strings.Split(err.(*jsonschema.ValidationError).GoString(), "\n") + assert.Contains(t, errors[1], "missing properties") + assert.Contains(t, errors[1], "poster_id") + } else { + t.Fatalf("got: type %T with value %s, want: *jsonschema.ValidationError", err, err) + } +} + +func TestMigrationJSON_MilestoneOK(t *testing.T) { + milestones := make([]*Milestone, 0, 10) + err := Load("file_format_testdata/milestones.json", &milestones, true) + assert.NoError(t, err) +} diff --git a/modules/migration/file_format_testdata/issue_a.json b/modules/migration/file_format_testdata/issue_a.json new file mode 100644 index 0000000000..33d7759f63 --- /dev/null +++ b/modules/migration/file_format_testdata/issue_a.json @@ -0,0 +1,14 @@ +[ + { + "number": 1, + "poster_id": 1, + "poster_name": "name_a", + "title": "title_a", + "content": "content_a", + "state": "closed", + "is_locked": false, + "created": "1985-04-12T23:20:50.52Z", + "updated": "1986-04-12T23:20:50.52Z", + "closed": "1987-04-12T23:20:50.52Z" + } +] diff --git a/modules/migration/file_format_testdata/issue_a.yml b/modules/migration/file_format_testdata/issue_a.yml new file mode 100644 index 0000000000..d03bfb31f2 --- /dev/null +++ b/modules/migration/file_format_testdata/issue_a.yml @@ -0,0 +1,10 @@ +- number: 1 + poster_id: 1 + poster_name: name_a + title: title_a + content: content_a + state: closed + is_locked: false + created: 2021-05-27T15:24:13+02:00 + updated: 2021-11-11T10:52:45+01:00 + closed: 2021-11-11T10:52:45+01:00 diff --git a/modules/migration/file_format_testdata/issue_b.json b/modules/migration/file_format_testdata/issue_b.json new file mode 100644 index 0000000000..2a824d42fd --- /dev/null +++ b/modules/migration/file_format_testdata/issue_b.json @@ -0,0 +1,5 @@ +[ + { + "number": 1 + } +] diff --git a/modules/migration/file_format_testdata/milestones.json b/modules/migration/file_format_testdata/milestones.json new file mode 100644 index 0000000000..8fb770d87b --- /dev/null +++ b/modules/migration/file_format_testdata/milestones.json @@ -0,0 +1,20 @@ +[ + { + "title": "title_a", + "description": "description_a", + "deadline": "1988-04-12T23:20:50.52Z", + "created": "1985-04-12T23:20:50.52Z", + "updated": "1986-04-12T23:20:50.52Z", + "closed": "1987-04-12T23:20:50.52Z", + "state": "closed" + }, + { + "title": "title_b", + "description": "description_b", + "deadline": "1998-04-12T23:20:50.52Z", + "created": "1995-04-12T23:20:50.52Z", + "updated": "1996-04-12T23:20:50.52Z", + "closed": null, + "state": "open" + } +] diff --git a/modules/migration/issue.go b/modules/migration/issue.go index 26812633f9..19781ad984 100644 --- a/modules/migration/issue.go +++ b/modules/migration/issue.go @@ -28,21 +28,21 @@ func (c BasicIssueContext) ForeignID() int64 { // Issue is a standard issue information type Issue struct { - Number int64 - PosterID int64 `yaml:"poster_id"` - PosterName string `yaml:"poster_name"` - PosterEmail string `yaml:"poster_email"` - Title string - Content string - Ref string - Milestone string - State string // closed, open - IsLocked bool `yaml:"is_locked"` - Created time.Time - Updated time.Time - Closed *time.Time - Labels []*Label - Reactions []*Reaction - Assignees []string + Number int64 `json:"number"` + PosterID int64 `yaml:"poster_id" json:"poster_id"` + PosterName string `yaml:"poster_name" json:"poster_name"` + PosterEmail string `yaml:"poster_email" json:"poster_email"` + Title string `json:"title"` + Content string `json:"content"` + Ref string `json:"ref"` + Milestone string `json:"milestone"` + State string `json:"state"` // closed, open + IsLocked bool `yaml:"is_locked" json:"is_locked"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Closed *time.Time `json:"closed"` + Labels []*Label `json:"labels"` + Reactions []*Reaction `json:"reactions"` + Assignees []string `json:"assignees"` Context IssueContext `yaml:"-"` } diff --git a/modules/migration/label.go b/modules/migration/label.go index 1a04a1dd3a..f49fbe3ee4 100644 --- a/modules/migration/label.go +++ b/modules/migration/label.go @@ -7,7 +7,7 @@ package migration // Label defines a standard label information type Label struct { - Name string - Color string - Description string + Name string `json:"name"` + Color string `json:"color"` + Description string `json:"description"` } diff --git a/modules/migration/milestone.go b/modules/migration/milestone.go index 209aafe6a7..f3b725de67 100644 --- a/modules/migration/milestone.go +++ b/modules/migration/milestone.go @@ -9,11 +9,11 @@ import "time" // Milestone defines a standard milestone type Milestone struct { - Title string - Description string - Deadline *time.Time - Created time.Time - Updated *time.Time - Closed *time.Time - State string // open, closed + Title string `json:"title"` + Description string `json:"description"` + Deadline *time.Time `json:"deadline"` + Created time.Time `json:"created"` + Updated *time.Time `json:"updated"` + Closed *time.Time `json:"closed"` + State string `json:"state"` // open, closed } diff --git a/modules/migration/reaction.go b/modules/migration/reaction.go index 004cff2f94..2ba44a15a2 100644 --- a/modules/migration/reaction.go +++ b/modules/migration/reaction.go @@ -6,7 +6,7 @@ package migration // Reaction represents a reaction to an issue/pr/comment. type Reaction struct { - UserID int64 `yaml:"user_id"` - UserName string `yaml:"user_name"` - Content string + UserID int64 `yaml:"user_id" json:"user_id"` + UserName string `yaml:"user_name" json:"user_name"` + Content string `json:"content"` } diff --git a/modules/migration/schemas/issue.json b/modules/migration/schemas/issue.json new file mode 100644 index 0000000000..25753c39e2 --- /dev/null +++ b/modules/migration/schemas/issue.json @@ -0,0 +1,114 @@ +{ + "title": "Issue", + "description": "Issues associated to a repository within a forge (Gitea, GitLab, etc.).", + + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "number": { + "description": "Unique identifier, relative to the repository.", + "type": "number" + }, + "poster_id": { + "description": "Unique identifier of the user who authored the issue.", + "type": "number" + }, + "poster_name": { + "description": "Name of the user who authored the issue.", + "type": "string" + }, + "poster_email": { + "description": "Email of the user who authored the issue.", + "type": "string" + }, + "title": { + "description": "Short description displayed as the title.", + "type": "string" + }, + "content": { + "description": "Long, multiline, description.", + "type": "string" + }, + "ref": { + "description": "Target branch in the repository.", + "type": "string" + }, + "milestone": { + "description": "Name of the milestone.", + "type": "string" + }, + "state": { + "description": "A 'closed' issue will not see any activity in the future, otherwise it is 'open'.", + "enum": [ + "closed", + "open" + ] + }, + "is_locked": { + "description": "A locked issue can only be modified by privileged users.", + "type": "boolean" + }, + "created": { + "description": "Creation time.", + "type": "string", + "format": "date-time" + }, + "updated": { + "description": "Last update time.", + "type": "string", + "format": "date-time" + }, + "closed": { + "description": "The last time 'state' changed to 'closed'.", + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "labels": { + "description": "List of labels.", + "type": "array", + "items": { + "$ref": "label.json" + } + }, + "reactions": { + "description": "List of reactions.", + "type": "array", + "items": { + "$ref": "reaction.json" + } + }, + "assignees": { + "description": "List of assignees.", + "type": "array", + "items": { + "description": "Name of a user assigned to the issue.", + "type": "string" + } + } + }, + "required": [ + "number", + "poster_id", + "poster_name", + "title", + "content", + "state", + "is_locked", + "created", + "updated" + ] + }, + + "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "http://example.com/issue.json", + "$$target": "issue.json" +} diff --git a/modules/migration/schemas/label.json b/modules/migration/schemas/label.json new file mode 100644 index 0000000000..561a2e3357 --- /dev/null +++ b/modules/migration/schemas/label.json @@ -0,0 +1,28 @@ +{ + "title": "Label", + "description": "Label associated to an issue.", + + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "description": "Name of the label, unique within the repository.", + "type": "string" + }, + "color": { + "description": "Color code of the label.", + "type": "string" + }, + "description": { + "description": "Long, multiline, description.", + "type": "string" + } + }, + "required": [ + "name" + ], + + "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "label.json", + "$$target": "label.json" +} diff --git a/modules/migration/schemas/milestone.json b/modules/migration/schemas/milestone.json new file mode 100644 index 0000000000..7024ef45d0 --- /dev/null +++ b/modules/migration/schemas/milestone.json @@ -0,0 +1,67 @@ +{ + "title": "Milestone", + "description": "Milestone associated to a repository within a forge.", + + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "title": { + "description": "Short description.", + "type": "string" + }, + "description": { + "description": "Long, multiline, description.", + "type": "string" + }, + "deadline": { + "description": "Deadline after which the milestone is overdue.", + "type": "string", + "format": "date-time" + }, + "created": { + "description": "Creation time.", + "type": "string", + "format": "date-time" + }, + "updated": { + "description": "Last update time.", + "type": "string", + "format": "date-time" + }, + "closed": { + "description": "The last time 'state' changed to 'closed'.", + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ] + }, + "state": { + "description": "A 'closed' issue will not see any activity in the future, otherwise it is 'open'.", + "enum": [ + "closed", + "open" + ] + } + }, + "required": [ + "title", + "description", + "deadline", + "created", + "updated", + "closed", + "state" + ] + }, + + "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "http://example.com/milestone.json", + "$$target": "milestone.json" +} diff --git a/modules/migration/schemas/reaction.json b/modules/migration/schemas/reaction.json new file mode 100644 index 0000000000..25652514be --- /dev/null +++ b/modules/migration/schemas/reaction.json @@ -0,0 +1,29 @@ +{ + "title": "Reaction", + "description": "Reaction associated to an issue or a comment.", + + "type": "object", + "additionalProperties": false, + "properties": { + "user_id": { + "description": "Unique identifier of the user who authored the reaction.", + "type": "number" + }, + "user_name": { + "description": "Name of the user who authored the reaction.", + "type": "string" + }, + "content": { + "description": "Representation of the reaction", + "type": "string" + } + }, + "required": [ + "user_id", + "content" + ], + + "$schema": "http://json-schema.org/draft-04/schema#", + "$id": "http://example.com/reaction.json", + "$$target": "reaction.json" +} diff --git a/modules/migration/schemas_bindata.go b/modules/migration/schemas_bindata.go new file mode 100644 index 0000000000..d0fef698b4 --- /dev/null +++ b/modules/migration/schemas_bindata.go @@ -0,0 +1,10 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +//go:build bindata +// +build bindata + +package migration + +//go:generate go run ../../build/generate-bindata.go ../../modules/migration/schemas migration bindata.go diff --git a/modules/migration/schemas_dynamic.go b/modules/migration/schemas_dynamic.go new file mode 100644 index 0000000000..c883fafe98 --- /dev/null +++ b/modules/migration/schemas_dynamic.go @@ -0,0 +1,40 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +//go:build !bindata +// +build !bindata + +package migration + +import ( + "io" + "net/url" + "os" + "path" + "path/filepath" +) + +func openSchema(s string) (io.ReadCloser, error) { + u, err := url.Parse(s) + if err != nil { + return nil, err + } + basename := path.Base(u.Path) + filename := basename + // + // Schema reference each other within the schemas directory but + // the tests run in the parent directory. + // + if _, err := os.Stat(filename); os.IsNotExist(err) { + filename = filepath.Join("schemas", basename) + // + // Integration tests run from the git root directory, not the + // directory in which the test source is located. + // + if _, err := os.Stat(filename); os.IsNotExist(err) { + filename = filepath.Join("modules/migration/schemas", basename) + } + } + return os.Open(filename) +} diff --git a/modules/migration/schemas_static.go b/modules/migration/schemas_static.go new file mode 100644 index 0000000000..10c83b313a --- /dev/null +++ b/modules/migration/schemas_static.go @@ -0,0 +1,17 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +//go:build bindata +// +build bindata + +package migration + +import ( + "io" + "path" +) + +func openSchema(filename string) (io.ReadCloser, error) { + return Assets.Open(path.Base(filename)) +} diff --git a/modules/private/restore_repo.go b/modules/private/restore_repo.go index 6f8eaf927f..347ed5e78a 100644 --- a/modules/private/restore_repo.go +++ b/modules/private/restore_repo.go @@ -17,24 +17,26 @@ import ( // RestoreParams structure holds a data for restore repository type RestoreParams struct { - RepoDir string - OwnerName string - RepoName string - Units []string + RepoDir string + OwnerName string + RepoName string + Units []string + Validation bool } // RestoreRepo calls the internal RestoreRepo function -func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units []string) (int, string) { +func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units []string, validation bool) (int, string) { reqURL := setting.LocalURL + "api/internal/restore_repo" req := newInternalRequest(ctx, reqURL, "POST") req.SetTimeout(3*time.Second, 0) // since the request will spend much time, don't timeout req = req.Header("Content-Type", "application/json") jsonBytes, _ := json.Marshal(RestoreParams{ - RepoDir: repoDir, - OwnerName: ownerName, - RepoName: repoName, - Units: units, + RepoDir: repoDir, + OwnerName: ownerName, + RepoName: repoName, + Units: units, + Validation: validation, }) req.Body(jsonBytes) resp, err := req.Response() diff --git a/routers/private/restore_repo.go b/routers/private/restore_repo.go index 8dfe569325..34e06e51c2 100644 --- a/routers/private/restore_repo.go +++ b/routers/private/restore_repo.go @@ -24,10 +24,11 @@ func RestoreRepo(ctx *myCtx.PrivateContext) { return } params := struct { - RepoDir string - OwnerName string - RepoName string - Units []string + RepoDir string + OwnerName string + RepoName string + Units []string + Validation bool }{} if err = json.Unmarshal(bs, ¶ms); err != nil { ctx.JSON(http.StatusInternalServerError, private.Response{ @@ -42,6 +43,7 @@ func RestoreRepo(ctx *myCtx.PrivateContext) { params.OwnerName, params.RepoName, params.Units, + params.Validation, ); err != nil { ctx.JSON(http.StatusInternalServerError, private.Response{ Err: err.Error(), diff --git a/services/migrations/dump.go b/services/migrations/dump.go index 01a946673e..1e4b3326b6 100644 --- a/services/migrations/dump.go +++ b/services/migrations/dump.go @@ -604,13 +604,13 @@ func updateOptionsUnits(opts *base.MigrateOptions, units []string) { } // RestoreRepository restore a repository from the disk directory -func RestoreRepository(ctx context.Context, baseDir, ownerName, repoName string, units []string) error { +func RestoreRepository(ctx context.Context, baseDir, ownerName, repoName string, units []string, validation bool) error { doer, err := user_model.GetAdminUser() if err != nil { return err } uploader := NewGiteaLocalUploader(ctx, doer, ownerName, repoName) - downloader, err := NewRepositoryRestorer(ctx, baseDir, ownerName, repoName) + downloader, err := NewRepositoryRestorer(ctx, baseDir, ownerName, repoName, validation) if err != nil { return err } diff --git a/services/migrations/restore.go b/services/migrations/restore.go index c927de0b2b..d30d90a6c4 100644 --- a/services/migrations/restore.go +++ b/services/migrations/restore.go @@ -19,23 +19,25 @@ import ( // RepositoryRestorer implements an Downloader from the local directory type RepositoryRestorer struct { base.NullDownloader - ctx context.Context - baseDir string - repoOwner string - repoName string + ctx context.Context + baseDir string + repoOwner string + repoName string + validation bool } // NewRepositoryRestorer creates a repository restorer which could restore repository from a dumped folder -func NewRepositoryRestorer(ctx context.Context, baseDir, owner, repoName string) (*RepositoryRestorer, error) { +func NewRepositoryRestorer(ctx context.Context, baseDir, owner, repoName string, validation bool) (*RepositoryRestorer, error) { baseDir, err := filepath.Abs(baseDir) if err != nil { return nil, err } return &RepositoryRestorer{ - ctx: ctx, - baseDir: baseDir, - repoOwner: owner, - repoName: repoName, + ctx: ctx, + baseDir: baseDir, + repoOwner: owner, + repoName: repoName, + validation: validation, }, nil } @@ -114,7 +116,7 @@ func (r *RepositoryRestorer) GetTopics() ([]string, error) { func (r *RepositoryRestorer) GetMilestones() ([]*base.Milestone, error) { milestones := make([]*base.Milestone, 0, 10) p := filepath.Join(r.baseDir, "milestone.yml") - _, err := os.Stat(p) + err := base.Load(p, &milestones, r.validation) if err != nil { if os.IsNotExist(err) { return nil, nil @@ -122,15 +124,6 @@ func (r *RepositoryRestorer) GetMilestones() ([]*base.Milestone, error) { return nil, err } - bs, err := os.ReadFile(p) - if err != nil { - return nil, err - } - - err = yaml.Unmarshal(bs, &milestones) - if err != nil { - return nil, err - } return milestones, nil } @@ -193,7 +186,7 @@ func (r *RepositoryRestorer) GetLabels() ([]*base.Label, error) { func (r *RepositoryRestorer) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { issues := make([]*base.Issue, 0, 10) p := filepath.Join(r.baseDir, "issue.yml") - _, err := os.Stat(p) + err := base.Load(p, &issues, r.validation) if err != nil { if os.IsNotExist(err) { return nil, true, nil @@ -201,15 +194,6 @@ func (r *RepositoryRestorer) GetIssues(page, perPage int) ([]*base.Issue, bool, return nil, false, err } - bs, err := os.ReadFile(p) - if err != nil { - return nil, false, err - } - - err = yaml.Unmarshal(bs, &issues) - if err != nil { - return nil, false, err - } for _, issue := range issues { issue.Context = base.BasicIssueContext(issue.Number) }