Store uploads on disk under DataDir and expose them in the web ticket view so .xlsx files can be previewed and saved as cell values without YouTrack or TUI sync. Co-authored-by: Cursor <cursoragent@cursor.com>
194 lines
4.7 KiB
Go
194 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"task/internal/config"
|
|
"task/internal/server"
|
|
"task/internal/sqlite"
|
|
"task/internal/version"
|
|
)
|
|
|
|
func main() {
|
|
listen := flag.String("listen", ":8080", "HTTP listen address")
|
|
dbPath := flag.String("db", "", "SQLite database path (default: <dataDir>/task-master.db)")
|
|
secure := flag.Bool("secure", false, "Set Secure flag on session cookies")
|
|
flag.Parse()
|
|
|
|
args := flag.Args()
|
|
if len(args) > 0 {
|
|
switch args[0] {
|
|
case "version", "--version", "-v":
|
|
fmt.Printf("tm_server %s\n", version.BuildVersion)
|
|
return
|
|
case "help", "--help", "-h":
|
|
printHelp()
|
|
return
|
|
case "import-json":
|
|
if err := runImportJSON(args[1:]); err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
|
|
printHelp()
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
if err := runServer(*listen, *dbPath, *secure); err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func openDB(dbPath string) (*sqlite.DB, *config.Config, error) {
|
|
cfg, err := config.LoadOrCreate()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("config: %w", err)
|
|
}
|
|
path := dbPath
|
|
if path == "" {
|
|
path = filepath.Join(cfg.DataDir, "task-master.db")
|
|
}
|
|
secret := []byte(os.Getenv("TASK_MASTER_SECRET"))
|
|
db, err := sqlite.Open(path, secret)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return db, cfg, nil
|
|
}
|
|
|
|
func bootstrapAdmin(db *sqlite.DB) error {
|
|
n, err := db.CountUsers()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n > 0 {
|
|
return nil
|
|
}
|
|
user := strings.TrimSpace(os.Getenv("TM_ADMIN_USER"))
|
|
pass := os.Getenv("TM_ADMIN_PASSWORD")
|
|
if user == "" || pass == "" {
|
|
fmt.Fprintln(os.Stderr, "no users in database; set TM_ADMIN_USER and TM_ADMIN_PASSWORD to create the first admin")
|
|
return nil
|
|
}
|
|
u, err := db.CreateUser(user, pass, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(os.Stderr, "created bootstrap admin %q (%s)\n", u.Username, u.ID)
|
|
return nil
|
|
}
|
|
|
|
func runServer(listen, dbPath string, secure bool) error {
|
|
db, cfg, err := openDB(dbPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer db.Close()
|
|
if err := bootstrapAdmin(db); err != nil {
|
|
return err
|
|
}
|
|
srv := server.New(db)
|
|
srv.DataDir = cfg.DataDir
|
|
srv.Secure = secure
|
|
if strings.EqualFold(os.Getenv("TM_SEED_DEMO"), "1") ||
|
|
strings.EqualFold(os.Getenv("TM_SEED_DEMO"), "true") {
|
|
if err := srv.SeedDemo(server.SeedDemoOptions{
|
|
ClientDataDir: cfg.DataDir,
|
|
BaseURL: server.SeedBaseURL(listen),
|
|
}); err != nil {
|
|
return fmt.Errorf("seed: %w", err)
|
|
}
|
|
projects, _ := srv.Projects.List()
|
|
fmt.Fprintf(os.Stderr, "demo seed ready (%d projects; login admin/admin, alice/alice)\n", len(projects))
|
|
}
|
|
fmt.Fprintf(os.Stderr, "tm_server %s listening on %s\n", version.BuildVersion, listen)
|
|
return http.ListenAndServe(listen, srv.Handler())
|
|
}
|
|
|
|
func runImportJSON(args []string) error {
|
|
fs := flag.NewFlagSet("import-json", flag.ExitOnError)
|
|
dataDir := fs.String("data", "", "JSON DataDir to import (default from config)")
|
|
dbPath := fs.String("db", "", "SQLite database path")
|
|
owner := fs.String("owner", "", "username to own imported projects")
|
|
_ = fs.Parse(args)
|
|
|
|
db, cfg, err := openDB(*dbPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer db.Close()
|
|
if err := bootstrapAdmin(db); err != nil {
|
|
return err
|
|
}
|
|
|
|
dir := *dataDir
|
|
if dir == "" {
|
|
dir = cfg.DataDir
|
|
}
|
|
var ownerID string
|
|
if *owner != "" {
|
|
u, err := db.GetUserByUsername(*owner)
|
|
if err != nil {
|
|
return fmt.Errorf("owner: %w", err)
|
|
}
|
|
ownerID = u.ID
|
|
} else {
|
|
users, err := db.ListUsers()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(users) == 0 {
|
|
return fmt.Errorf("no users; create admin first (TM_ADMIN_USER/TM_ADMIN_PASSWORD)")
|
|
}
|
|
for _, u := range users {
|
|
if u.IsAdmin {
|
|
ownerID = u.ID
|
|
break
|
|
}
|
|
}
|
|
if ownerID == "" {
|
|
ownerID = users[0].ID
|
|
}
|
|
}
|
|
|
|
srv := server.New(db)
|
|
fmt.Fprintf(os.Stderr, "importing from %s …\n", dir)
|
|
if err := srv.ImportJSONDataDir(dir, ownerID); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintln(os.Stderr, "import done")
|
|
return nil
|
|
}
|
|
|
|
func printHelp() {
|
|
fmt.Print(`tm_server — task-master web server
|
|
|
|
Usage:
|
|
tm_server [flags]
|
|
tm_server import-json [-data DIR] [-db PATH] [-owner USER]
|
|
tm_server version
|
|
tm_server help
|
|
|
|
Flags:
|
|
-listen ADDR HTTP listen address (default :8080)
|
|
-db PATH SQLite path (default <dataDir>/task-master.db)
|
|
-secure Set Secure cookie flag
|
|
|
|
Env:
|
|
TASK_MASTER_HOME / TASK_MASTER_DATA_DIR same as tm
|
|
TASK_MASTER_SECRET encrypts remote tokens
|
|
TM_ADMIN_USER / TM_ADMIN_PASSWORD bootstrap first admin
|
|
TM_SEED_DEMO=1 seed demo projects + wire TUI (Test, Website Relaunch)
|
|
TM_SEED_BASE_URL override base URL written into TUI meta.json
|
|
`)
|
|
}
|