WIP: Add Kanboard migrator #1607

Draft
jackymancs4 wants to merge 3 commits from jackymancs4/vikunja-api:feat/kanboard-migrator into main
8 changed files with 219 additions and 2 deletions

View File

@ -18,7 +18,7 @@ The interface makes it possible to use helper methods which handle http and focu
There are two ways of migrating data from another service:
1. Through the auth-based flow where the user gives you access to their data at the third-party service through an oauth flow. You can then call the service's api on behalf of your user to get all the data. The Todoist, Trello and Microsoft To-Do Migrators use this pattern.
1. Through the auth-based flow where the user gives you access to their data at the third-party service through an oauth flow. You can then call the service's api on behalf of your user to get all the data. The Todoist, Kanboard, Trello and Microsoft To-Do Migrators use this pattern.
2. A file migration where the user uploads a file obtained from some third-party service. In your migrator, you need to parse the file and create the projects, tasks etc. The Vikunja File Import uses this pattern.
To differentiate the two, there are two different interfaces you must implement.

View File

@ -1022,6 +1022,15 @@ Full path: `migration.trello`
Environment path: `VIKUNJA_MIGRATION_TRELLO`
### kanboard
Default: `<empty>`
Full path: `migration.kanboard`
Environment path: `VIKUNJA_MIGRATION_KANBOARD`
### microsofttodo
Default: `<empty>`

1
go.mod
View File

@ -164,6 +164,7 @@ require (
github.com/urfave/cli/v2 v2.3.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/ybbus/jsonrpc/v2 v2.1.7 // indirect
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 // indirect
go.opentelemetry.io/otel v1.15.0 // indirect
go.opentelemetry.io/otel/trace v1.15.0 // indirect

3
go.sum
View File

@ -595,6 +595,7 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
@ -769,6 +770,8 @@ github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/ybbus/jsonrpc/v2 v2.1.7 h1:QjoXuZhkXZ3oLBkrONBe2avzFkYeYLorpeA+d8175XQ=
github.com/ybbus/jsonrpc/v2 v2.1.7/go.mod h1:rIuG1+ORoiqocf9xs/v+ecaAVeo3zcZHQgInyKFMeg0=
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts=
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=

View File

@ -142,6 +142,8 @@ const (
MigrationMicrosoftTodoClientID Key = `migration.microsofttodo.clientid`
MigrationMicrosoftTodoClientSecret Key = `migration.microsofttodo.clientsecret`
MigrationMicrosoftTodoRedirectURL Key = `migration.microsofttodo.redirecturl`
MigrationKanboardEnable Key = `migration.kanboard.enable`
MigrationKanboardApiInsecure Key = `migration.kanboard.insecure`
CorsEnable Key = `cors.enable`
CorsOrigins Key = `cors.origins`

View File

@ -0,0 +1,187 @@
// 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 kanboard
import (
"crypto/tls"
"encoding/base64"
"net/http"
"github.com/ybbus/jsonrpc/v2"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/migration"
"code.vikunja.io/api/pkg/user"
)
// Migration represents the kanboard migration struct
type Migration struct {
Endpoint string `json:"endpoint"`
Username string `json:"username"`
Password string `json:"password"`
}
type kanboardBoard struct {
ID string `json:"id"`
Name string `json:"name"`
IsActive string `json:"is_active"`
Token string `json:"token"`
LastModified string `json:"last_modified"`
IsPublic string `json:"is_public"`
IsPrivate string `json:"is_private"`
DefaultSwimlane string `json:"default_swimlane"`
ShowDefaultSwimlane string `json:"show_default_swimlane"`
Description interface{} `json:"description"`
Identifier string `json:"identifier"`
URL struct {
Board string `json:"board"`
Calendar string `json:"calendar"`
List string `json:"list"`
} `json:"url"`
}
func init() {
}
// Name is used to get the name of the kanboard migration - we're using the docs here to annotate the status route.
// @Summary Get migration status
// @Description Returns if the current user already did the migation or not. This is useful to show a confirmation message in the frontend if the user is trying to do the same migration again.
// @tags migration
// @Produce json
// @Security JWTKeyAuth
// @Success 200 {object} migration.Status "The migration status"
// @Failure 500 {object} models.Message "Internal server error"
// @Router /migration/kanboard/status [get]
func (m *Migration) Name() string {
return "kanboard"
}
// AuthURL returns the url users need to authenticate against
// @Summary Get the auth url from kanboard
// @Description Returns the auth url where the user needs to get its auth code. This code can then be used to migrate everything from kanboard to Vikunja.
// @tags migration
// @Produce json
// @Security JWTKeyAuth
// @Success 200 {object} handler.AuthURL "The auth url."
// @Failure 500 {object} models.Message "Internal server error"
// @Router /migration/kanboard/auth [get]
func (m *Migration) AuthURL() string {
return ""
}
func basicAuth(username, password string) string {
auth := username + ":" + password
return base64.StdEncoding.EncodeToString([]byte(auth))
}
func getKanboardData(endpoint string, username string, password string, allowInsecure bool) (kanboardData []kanboardBoard, err error) {
var tr *http.Transport
if allowInsecure {
tr = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
} else {
tr = &http.Transport{}
}
client := &http.Client{Transport: tr}
auth := make(map[string]string)
auth["Authorization"] = "Basic " + basicAuth(username, password)
opts := jsonrpc.RPCClientOpts{
HTTPClient: client,
CustomHeaders: auth,
}
rpcClient := jsonrpc.NewClientWithOpts(endpoint, &opts)
err = rpcClient.CallFor(&kanboardData, "getAllProjects")
return
}
// Converts all previously obtained data from kanboard into the vikunja format.
// `kanboardData` should contain all boards with their projects and cards respectively.
func convertKanboardDataToVikunja(kanboardData []kanboardBoard) (fullVikunjaHierachie []*models.ProjectWithTasksAndBuckets, err error) {
log.Debugf("[Kanboard Migration] ")
fullVikunjaHierachie = []*models.ProjectWithTasksAndBuckets{
{
Project: models.Project{
Title: "Imported from Kanboard",
},
ChildProjects: []*models.ProjectWithTasksAndBuckets{},
},
}
log.Debugf("[Kanboard Migration] Converting %d boards to vikunja projects", len(kanboardData))
return
}
// Migrate gets all tasks from kanboard for a user and puts them into vikunja
// @Summary Migrate all projects, tasks etc. from kanboard
// @Description Migrates all projects, tasks, notes, reminders, subtasks and files from kanboard to vikunja.
// @tags migration
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param migrationCode body kanboard.Migration true "The auth token previously obtained from the auth url. See the docs for /migration/kanboard/auth."
// @Success 200 {object} models.Message "A message telling you everything was migrated successfully."
// @Failure 500 {object} models.Message "Internal server error"
// @Router /migration/kanboard/migrate [post]
func (m *Migration) Migrate(u *user.User) (err error) {
log.Debugf("[Kanboard Migration] Starting migration for user %d", u.ID)
log.Debugf("[Kanboard Migration] Getting all kanboard data for user %d", u.ID)
kanboardData, err := getKanboardData(
m.Endpoint,
m.Username,
m.Password,
config.MigrationKanboardApiInsecure.GetBool(),
)
if err != nil {
return
}
log.Debugf("[Kanboard Migration] Got all kanboard data for user %d", u.ID)
log.Debugf("[Kanboard Migration] Start converting kanboard data for user %d", u.ID)
fullVikunjaHierachie, err := convertKanboardDataToVikunja(kanboardData)
if err != nil {
return
}
log.Debugf("[Kanboard Migration] Done migrating kanboard data for user %d", u.ID)
log.Debugf("[Kanboard Migration] Start inserting kanboard data for user %d", u.ID)
err = migration.InsertFromStructure(fullVikunjaHierachie, u)
if err != nil {
return
}
log.Debugf("[Kanboard Migration] Done inserting kanboard data for user %d", u.ID)
log.Debugf("[Kanboard Migration] Migration done for user %d", u.ID)
return nil
}

View File

@ -22,6 +22,7 @@ import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/modules/auth/openid"
"code.vikunja.io/api/pkg/modules/migration/kanboard"
microsofttodo "code.vikunja.io/api/pkg/modules/migration/microsoft-todo"
"code.vikunja.io/api/pkg/modules/migration/ticktick"
"code.vikunja.io/api/pkg/modules/migration/todoist"
@ -134,7 +135,10 @@ func Info(c echo.Context) error {
m := &microsofttodo.Migration{}
info.AvailableMigrators = append(info.AvailableMigrators, m.Name())
}
if config.MigrationKanboardEnable.GetBool() {
m := &kanboard.Migration{}
info.AvailableMigrators = append(info.AvailableMigrators, m.Name())
}
if config.BackgroundsEnabled.GetBool() {
if config.BackgroundsUploadEnabled.GetBool() {
info.EnabledBackgroundProviders = append(info.EnabledBackgroundProviders, "upload")

View File

@ -69,6 +69,7 @@ import (
"code.vikunja.io/api/pkg/modules/background/upload"
"code.vikunja.io/api/pkg/modules/migration"
migrationHandler "code.vikunja.io/api/pkg/modules/migration/handler"
"code.vikunja.io/api/pkg/modules/migration/kanboard"
microsofttodo "code.vikunja.io/api/pkg/modules/migration/microsoft-todo"
"code.vikunja.io/api/pkg/modules/migration/ticktick"
"code.vikunja.io/api/pkg/modules/migration/todoist"
@ -597,6 +598,16 @@ func registerMigrations(m *echo.Group) {
trelloMigrationHandler.RegisterRoutes(m)
}
// Kanboard
if config.MigrationKanboardEnable.GetBool() {
kanboardMigrationHandler := &migrationHandler.MigrationWeb{
MigrationStruct: func() migration.Migrator {
return &kanboard.Migration{}
},
}
kanboardMigrationHandler.RegisterRoutes(m)
}
// Microsoft Todo
if config.MigrationMicrosoftTodoEnable.GetBool() {
microsoftTodoMigrationHandler := &migrationHandler.MigrationWeb{