vikunja/pkg/modules/migration/create_from_structure.go

474 lines
13 KiB
Go

// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"bytes"
"io"
"xorm.io/xorm"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/background/handler"
"code.vikunja.io/api/pkg/user"
)
// InsertFromStructure takes a fully nested Vikunja data structure and a user and then creates everything for this user
// (Projects, tasks, etc. Even attachments and relations.)
func InsertFromStructure(str []*models.ProjectWithTasksAndBuckets, user *user.User) (err error) {
s := db.NewSession()
defer s.Close()
err = insertFromStructure(s, str, user)
if err != nil {
log.Errorf("[creating structure] Error while creating structure: %s", err.Error())
_ = s.Rollback()
return err
}
return s.Commit()
}
func insertFromStructure(s *xorm.Session, str []*models.ProjectWithTasksAndBuckets, user *user.User) (err error) {
log.Debugf("[creating structure] Creating %d projects", len(str))
labels := make(map[string]*models.Label)
archivedProjects := []int64{}
childRelations := make(map[int64][]int64) // old id is the key, slice of old children ids
projectsByOldID := make(map[int64]*models.Project) // old id is the key
// Create all projects
for i, p := range str {
oldID := p.ID
if p.ParentProjectID != 0 {
childRelations[p.ParentProjectID] = append(childRelations[p.ParentProjectID], oldID)
p.ParentProjectID = 0
}
p.ID = 0
for _, view := range p.Views {
view.ProjectID = 0
}
err = createProject(s, p, &archivedProjects, labels, user)
if err != nil {
return err
}
projectsByOldID[oldID] = &str[i].Project
}
// parent / child relations
for parentID, children := range childRelations {
parent, has := projectsByOldID[parentID]
if !has {
log.Debugf("[creating structure] could not find parentID project with old id %d", parentID)
continue
}
for _, childID := range children {
child, has := projectsByOldID[childID]
if !has {
log.Debugf("[creating structure] could not find child project with old id %d for parent project with old id %d", childID, parentID)
continue
}
child.ParentProjectID = parent.ID
err = child.Update(s, user)
if err != nil {
return err
}
}
}
if len(archivedProjects) > 0 {
_, err = s.
Cols("is_archived").
In("id", archivedProjects).
Update(&models.Project{IsArchived: true})
if err != nil {
return err
}
}
log.Debugf("[creating structure] Done inserting new task structure")
return nil
}
func createProject(s *xorm.Session, project *models.ProjectWithTasksAndBuckets, archivedProjectIDs *[]int64, labels map[string]*models.Label, user *user.User) (err error) {
err = createProjectWithEverything(s, project, archivedProjectIDs, labels, user)
if err != nil {
return err
}
log.Debugf("[creating structure] Created project %d", project.ID)
return
}
func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTasksAndBuckets, archivedProjects *[]int64, labels map[string]*models.Label, user *user.User) (err error) {
// The tasks and bucket slices are going to be reset during the creation of the project, so we rescue it here
// to be able to still loop over them aftere the project was created.
tasks := project.Tasks
originalBuckets := project.Buckets
originalBackgroundInformation := project.BackgroundInformation
needsDefaultBucket := false
oldViews := project.Views
// Saving the archived status to archive the project again after creating it
var wasArchived bool
if project.IsArchived {
wasArchived = true
project.IsArchived = false
}
project.ID = 0
err = project.Create(s, user)
if err != nil && models.IsErrProjectIdentifierIsNotUnique(err) {
project.Identifier = ""
err = project.Create(s, user)
}
if err != nil {
return
}
if wasArchived {
*archivedProjects = append(*archivedProjects, project.ID)
}
log.Debugf("[creating structure] Created project %d", project.ID)
bf, is := originalBackgroundInformation.(*bytes.Buffer)
if is {
backgroundFile := bytes.NewReader(bf.Bytes())
log.Debugf("[creating structure] Creating a background file for project %d", project.ID)
err = handler.SaveBackgroundFile(s, user, &project.Project, backgroundFile, "", uint64(backgroundFile.Len()))
if err != nil {
log.Errorf("[creating structure] Could not create background for project %d, error was %v", project.ID, err)
}
log.Debugf("[creating structure] Created a background file for project %d", project.ID)
}
// Create all buckets
bucketsByOldID := make(map[int64]*models.Bucket) // old bucket id is the key
if len(project.Buckets) > 0 {
log.Debugf("[creating structure] Creating %d buckets", len(project.Buckets))
}
for _, bucket := range originalBuckets {
oldID := bucket.ID
bucket.ID = 0 // We want a new id
bucket.ProjectID = project.ID
err = bucket.Create(s, user)
if err != nil {
return
}
bucketsByOldID[oldID] = bucket
log.Debugf("[creating structure] Created bucket %d, old ID was %d", bucket.ID, oldID)
}
// Create all views, create default views if we don't have any
viewsByOldIDs := make(map[int64]*models.ProjectView, len(oldViews))
if len(oldViews) > 0 {
for _, view := range oldViews {
oldID := view.ID
view.ID = 0
if view.DefaultBucketID != 0 {
bucket, has := bucketsByOldID[view.DefaultBucketID]
if has {
view.DefaultBucketID = bucket.ID
}
}
if view.DoneBucketID != 0 {
bucket, has := bucketsByOldID[view.DoneBucketID]
if has {
view.DoneBucketID = bucket.ID
}
}
view.ProjectID = project.ID
err = view.Create(s, user)
if err != nil {
return
}
viewsByOldIDs[oldID] = view
}
} else {
// Only using the default views
// Add all buckets to the default kanban view
for _, view := range project.Views {
if view.ViewKind == models.ProjectViewKindKanban {
for _, b := range bucketsByOldID {
b.ProjectViewID = view.ID
err = b.Update(s, user)
if err != nil {
return
}
}
break
}
}
}
log.Debugf("[creating structure] Creating %d tasks", len(tasks))
setBucketOrDefault := func(task *models.Task) {
bucket, exists := bucketsByOldID[task.BucketID]
if exists {
task.BucketID = bucket.ID
} else if task.BucketID > 0 {
log.Debugf("[creating structure] No bucket created for original bucket id %d", task.BucketID)
task.BucketID = 0
}
if !exists || task.BucketID == 0 {
needsDefaultBucket = true
}
}
tasksByOldID := make(map[int64]*models.TaskWithComments, len(tasks))
newTaskIDs := []int64{}
// Create all tasks
for i, t := range tasks {
setBucketOrDefault(&tasks[i].Task)
oldid := t.ID
t.ProjectID = project.ID
err = t.Create(s, user)
if err != nil && models.IsErrTaskCannotBeEmpty(err) {
continue
}
newTaskIDs = append(newTaskIDs, t.ID)
if err != nil {
return
}
tasksByOldID[oldid] = t
log.Debugf("[creating structure] Created task %d", t.ID)
if len(t.RelatedTasks) > 0 {
log.Debugf("[creating structure] Creating %d related task kinds", len(t.RelatedTasks))
}
// Create all relation for each task
for kind, tasks := range t.RelatedTasks {
if len(tasks) > 0 {
log.Debugf("[creating structure] Creating %d related tasks for kind %v", len(tasks), kind)
}
for _, rt := range tasks {
// First create the related tasks if they do not exist
if _, exists := tasksByOldID[rt.ID]; !exists || rt.ID == 0 {
oldid := rt.ID
setBucketOrDefault(rt)
rt.ProjectID = t.ProjectID
err = rt.Create(s, user)
if err != nil {
return
}
tasksByOldID[oldid] = &models.TaskWithComments{Task: *rt}
log.Debugf("[creating structure] Created related task %d", rt.ID)
}
// Then create the relation
taskRel := &models.TaskRelation{
TaskID: t.ID,
OtherTaskID: rt.ID,
RelationKind: kind,
}
if ttt, exists := tasksByOldID[rt.ID]; exists {
taskRel.OtherTaskID = ttt.ID
}
err = taskRel.Create(s, user)
if err != nil && !models.IsErrRelationAlreadyExists(err) {
return
}
log.Debugf("[creating structure] Created task relation between task %d and %d", t.ID, rt.ID)
}
}
// Create all attachments for each task
if len(t.Attachments) > 0 {
log.Debugf("[creating structure] Creating %d attachments", len(t.Attachments))
}
for _, a := range t.Attachments {
// Check if we have a file to create
if len(a.File.FileContent) > 0 {
oldID := a.ID
a.ID = 0
a.TaskID = t.ID
fr := io.NopCloser(bytes.NewReader(a.File.FileContent))
err = a.NewAttachment(s, fr, a.File.Name, a.File.Size, user)
if err != nil {
return
}
log.Debugf("[creating structure] Created new attachment %d", a.ID)
if t.CoverImageAttachmentID == oldID {
t.CoverImageAttachmentID = a.ID
err = t.Update(s, user)
if err != nil {
return
}
}
}
}
// Create all labels
for _, label := range t.Labels {
// Check if we already have a label with that name + color combination and use it
// If not, create one and save it for later
var lb *models.Label
var exists bool
if label == nil {
continue
}
lb, exists = labels[label.Title+label.HexColor]
if !exists {
err = label.Create(s, user)
if err != nil {
return err
}
log.Debugf("[creating structure] Created new label %d", label.ID)
labels[label.Title+label.HexColor] = label
lb = label
}
lt := &models.LabelTask{
LabelID: lb.ID,
TaskID: t.ID,
}
err = lt.Create(s, user)
if err != nil && !models.IsErrLabelIsAlreadyOnTask(err) {
return err
}
log.Debugf("[creating structure] Associated task %d with label %d", t.ID, lb.ID)
}
// Comments
for _, comment := range t.Comments {
comment.TaskID = t.ID
comment.ID = 0
err = comment.CreateWithTimestamps(s, user)
if err != nil {
return
}
log.Debugf("[creating structure] Created new comment %d", comment.ID)
}
}
// All tasks brought their own bucket with them, therefore the newly created default bucket is just extra space
if !needsDefaultBucket {
b := &models.Bucket{ProjectID: project.ID}
for _, view := range project.Views {
if view.ViewKind == models.ProjectViewKindKanban {
b.ProjectViewID = view.ID
break
}
}
bucketsIn, _, _, err := b.ReadAll(s, user, "", 1, 1)
if err != nil {
return err
}
buckets := bucketsIn.([]*models.Bucket)
var newBacklogBucket *models.Bucket
for _, b := range buckets {
if b.Title == "Backlog" {
newBacklogBucket = b
newBacklogBucket.ProjectID = project.ID
break
}
}
err = newBacklogBucket.Delete(s, user)
if err != nil && !models.IsErrCannotRemoveLastBucket(err) {
return err
}
}
if len(viewsByOldIDs) > 0 {
newPositions := []*models.TaskPosition{}
for _, pos := range project.Positions {
_, hasTask := tasksByOldID[pos.TaskID]
_, hasView := viewsByOldIDs[pos.ProjectViewID]
if !hasTask || !hasView {
continue
}
newPositions = append(newPositions, &models.TaskPosition{
TaskID: tasksByOldID[pos.TaskID].ID,
ProjectViewID: viewsByOldIDs[pos.ProjectViewID].ID,
Position: pos.Position,
})
}
if len(newPositions) > 0 {
_, err = s.In("task_id", newTaskIDs).Delete(&models.TaskPosition{})
if err != nil {
return
}
_, err = s.Insert(newPositions)
if err != nil {
return
}
}
newTaskBuckets := make([]*models.TaskBucket, 0, len(project.TaskBuckets))
for _, tb := range project.TaskBuckets {
_, hasTask := tasksByOldID[tb.TaskID]
_, hasBucket := bucketsByOldID[tb.BucketID]
if !hasTask || !hasBucket {
continue
}
newTaskBuckets = append(newTaskBuckets, &models.TaskBucket{
TaskID: tasksByOldID[tb.TaskID].ID,
BucketID: bucketsByOldID[tb.BucketID].ID,
ProjectViewID: bucketsByOldID[tb.BucketID].ProjectViewID,
})
}
if len(newTaskBuckets) > 0 {
_, err = s.In("task_id", newTaskIDs).Delete(&models.TaskBucket{})
if err != nil {
return
}
_, err = s.Insert(newTaskBuckets)
if err != nil {
return
}
}
}
project.Tasks = tasks
project.Buckets = originalBuckets
return nil
}