0123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
|
// Copyright (C) 2023, Jakob Wakeling
// All rights reserved.
package goit
import (
"archive/zip"
"database/sql"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"log"
"os"
"path/filepath"
"slices"
"strings"
"time"
"github.com/Jamozed/Goit/res"
"github.com/Jamozed/Goit/src/cron"
"github.com/Jamozed/Goit/src/util"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/transport"
_ "github.com/mattn/go-sqlite3"
)
var Conf config
var db *sql.DB
var Favicon []byte
var Cron *cron.Cron
var Reserved []string = []string{"admin", "repo", "static", "user"}
var StartTime = time.Now()
func Goit() error {
if conf, err := loadConfig(); err != nil {
return err
} else {
Conf = conf
}
if err := os.MkdirAll(Conf.LogsPath, 0o777); err != nil {
return fmt.Errorf("[config] %w", err)
}
logFile, err := os.Create(filepath.Join(Conf.LogsPath, fmt.Sprint("goit_", time.Now().Unix(), ".log")))
if err != nil {
log.Fatalln("[log]", err.Error())
}
log.SetOutput(io.MultiWriter(os.Stderr, logFile))
log.Println("Starting Goit", res.Version)
log.Println("[Config] using data path:", Conf.DataPath)
if err := os.MkdirAll(Conf.DataPath, 0o777); err != nil {
return fmt.Errorf("[config] %w", err)
}
if dat, err := os.ReadFile(filepath.Join(Conf.DataPath, "favicon.png")); err != nil {
log.Println("[favicon]", err.Error())
} else {
Favicon = dat
}
if db, err = sql.Open("sqlite3", filepath.Join(Conf.DataPath, "goit.db?_timeout=5000")); err != nil {
return fmt.Errorf("[database] %w", err)
}
/* Update the database if necessary */
if err := updateDatabase(db); err != nil {
return fmt.Errorf("[database] %w", err)
}
/* Create an admin user if one does not exist */
if exists, err := UserExists("admin"); err != nil {
log.Println("[admin:exists]", err.Error())
err = nil /* ignored */
} else if !exists {
if salt, err := Salt(); err != nil {
log.Println("[admin:salt]", err.Error())
err = nil /* ignored */
} else if _, err = db.Exec(
"INSERT INTO users (id, name, name_full, pass, pass_algo, salt, is_admin) VALUES (?, ?, ?, ?, ?, ?, ?)",
0, "admin", "Administrator", Hash("admin", salt), "argon2", salt, true,
); err != nil {
log.Println("[admin:INSERT]", err.Error())
err = nil /* ignored */
}
}
/* Initialise and start the cron service */
Cron = cron.New()
Cron.Start()
/* Periodically clean up expired sessions */
Cron.Add(-1, cron.Hourly, CleanupSessions)
/* Add cron jobs for mirror repositories */
repos, err := GetRepos()
if err != nil {
return err
}
for _, r := range repos {
if r.IsMirror {
util.Debugln("Adding mirror cron job for", r.Name)
rid, name := r.Id, r.Name
Cron.Add(r.Id, cron.Daily, func() {
if err := Pull(rid); err != nil {
log.Println("[cron:mirror]", rid, name, err.Error())
} else {
log.Println("[cron:mirror] updated", rid, name)
}
})
}
}
Cron.Update()
return nil
}
func RepoPath(name string, abs bool) string {
return util.If(abs, filepath.Join(Conf.DataPath, "repos", name+".git"), filepath.Join(name+".git"))
}
func IsLegal(s string) bool {
for i := 0; i < len(s); i += 1 {
if !slices.Contains([]byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~/"), s[i]) {
return false
}
}
return true
}
func Backup() error {
if conf, err := loadConfig(); err != nil {
return err
} else {
Conf = conf
}
data := struct {
Users []User `json:"users"`
Repos []Repo `json:"repos"`
}{}
bdir := filepath.Join(Conf.DataPath, "backup")
if err := os.MkdirAll(bdir, 0o777); err != nil {
return err
}
db, err := sql.Open("sqlite3", filepath.Join(Conf.DataPath, "goit.db?_timeout=5000&_txlock=immediate"))
if err != nil {
return err
}
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
/* Dump users */
rows, err := tx.Query("SELECT id, name, name_full, pass, pass_algo, salt, is_admin FROM users")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
u := User{}
if err := rows.Scan(&u.Id, &u.Name, &u.FullName, &u.Pass, &u.PassAlgo, &u.Salt, &u.IsAdmin); err != nil {
return err
}
data.Users = append(data.Users, u)
}
/* Dump repositories */
rows, err = tx.Query(
"SELECT id, owner_id, name, description, default_branch, upstream, visibility, is_mirror FROM repos",
)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
r := Repo{}
if err := rows.Scan(
&r.Id, &r.OwnerId, &r.Name, &r.Description, &r.DefaultBranch, &r.Upstream, &r.Visibility, &r.IsMirror,
); err != nil {
return err
}
data.Repos = append(data.Repos, r)
}
/* Open an output ZIP file */
ts := "goit_" + time.Now().UTC().Format("20060102T150405Z")
log.Println("Backing up to", filepath.Join(bdir, ts+".zip"))
zf, err := os.Create(filepath.Join(bdir, ts+".zip"))
if err != nil {
return err
}
defer zf.Close()
zw := zip.NewWriter(zf)
defer zw.Close()
/* Copy repositories to ZIP */
tempdir, err := os.MkdirTemp(os.TempDir(), "goit-")
if err != nil {
return err
}
defer os.RemoveAll(tempdir)
for _, r := range data.Repos {
t0 := time.Now()
cd := filepath.Join(tempdir, RepoPath(r.Name, false))
gr, err := git.PlainClone(cd, true, &git.CloneOptions{
URL: RepoPath(r.Name, true), Mirror: true,
})
if err != nil {
if errors.Is(err, transport.ErrRepositoryNotFound) {
log.Println("Skipping", r.Name, "as it does not exist")
continue
}
if errors.Is(err, transport.ErrEmptyRemoteRepository) {
log.Println("Skipping", r.Name, "as it is empty")
continue
}
return err
}
if err := gr.DeleteRemote("origin"); err != nil {
return fmt.Errorf("%s %w", cd, err)
}
/* Walk duplicated repository and add it to the ZIP */
if err = filepath.WalkDir(cd, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
info, err := d.Info()
if err != nil {
return err
}
head, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
head.Name = filepath.Join(ts, "repos", strings.TrimPrefix(path, tempdir))
if d.IsDir() {
head.Name += "/"
} else {
head.Method = zip.Store
}
w, err := zw.CreateHeader(head)
if err != nil {
return err
}
if !d.IsDir() {
fi, err := os.Open(path)
if err != nil {
return err
}
if _, err := io.Copy(w, fi); err != nil {
return err
}
}
return nil
}); err != nil {
return err
}
os.RemoveAll(cd)
log.Println("Backed up", r.Name, "in", time.Since(t0))
}
/* Write database as JSON to ZIP */
if b, err := json.MarshalIndent(data, "", "\t"); err != nil {
return err
} else if w, err := zw.Create(filepath.Join(ts, "goit.json")); err != nil {
return err
} else if _, err := w.Write(b); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
|