Files
GitOcean-Old/internal/app/server.go
T

182 lines
6.6 KiB
Go

package app
import (
"database/sql"
"errors"
"flag"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"gitocean/internal/dbutil"
)
// ---------------- Server boot and migrations ----------------
func runServer(args []string) error {
if len(args) > 0 && args[0] == "users" {
return cliServerUsers(args[1:])
}
fs := flag.NewFlagSet("server", flag.ExitOnError)
configFile := fs.String("config", defaultServerConfigPath(), "server config file")
addr := fs.String("addr", "", "HTTP listen address")
dsn := fs.String("dsn", os.Getenv("GITOOCEAN_DSN"), "MySQL DSN")
storage := fs.String("storage", "", "storage directory")
publicURL := fs.String("public-url", "", "public server URL")
if err := fs.Parse(args); err != nil {
return err
}
cfg, _ := loadServerConfig(*configFile)
if *addr != "" {
cfg.Addr = *addr
}
if *dsn != "" {
cfg.MySQLDSN = *dsn
}
if *storage != "" {
cfg.Storage = *storage
}
if *publicURL != "" {
cfg.PublicURL = *publicURL
}
if cfg.Addr == "" {
cfg.Addr = ":8080"
}
if cfg.Storage == "" {
cfg.Storage = defaultStorage
}
if cfg.PublicURL == "" {
cfg.PublicURL = defaultServerURL
}
if cfg.MySQLDSN == "" {
return errors.New("--dsn, GITOOCEAN_DSN, or server config mysql_dsn is required; run gitocean init")
}
db, err := openMySQLAndCreateDatabaseIfMissing(cfg.MySQLDSN)
if err != nil {
return err
}
defer db.Close()
if err := migrate(db); err != nil {
return err
}
if err := os.MkdirAll(filepath.Join(cfg.Storage, "repos"), 0755); err != nil {
return err
}
s := &Server{db: db, storage: cfg.Storage, publicURL: strings.TrimRight(cfg.PublicURL, "/")}
fmt.Printf("gitocean listening on %s, storage=%s, public_url=%s\n", cfg.Addr, cfg.Storage, s.publicURL)
return http.ListenAndServe(cfg.Addr, s)
}
func openMySQLAndCreateDatabaseIfMissing(dsn string) (*sql.DB, error) {
return dbutil.OpenMySQLAndCreateDatabaseIfMissing(dsn)
}
func quoteMySQLIdentifier(name string) string {
return dbutil.QuoteMySQLIdentifier(name)
}
func migrate(db *sql.DB) error {
stmts := []string{
`CREATE TABLE IF NOT EXISTS users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
username VARCHAR(32) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
is_admin BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
`CREATE TABLE IF NOT EXISTS auth_tokens (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
token_hash CHAR(64) NOT NULL UNIQUE,
expires_at TIMESTAMP NOT NULL,
revoked_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_auth_tokens_user_id (user_id),
INDEX idx_auth_tokens_expires_at (expires_at),
CONSTRAINT fk_auth_tokens_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
`CREATE TABLE IF NOT EXISTS repositories (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
owner_user_id BIGINT NOT NULL,
name VARCHAR(100) NOT NULL,
visibility ENUM('public','private') NOT NULL,
description TEXT NOT NULL,
default_branch VARCHAR(200) NOT NULL DEFAULT 'main',
archived BOOLEAN NOT NULL DEFAULT false,
forked_from_repository_id BIGINT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uniq_owner_name (owner_user_id, name),
INDEX idx_repositories_name (name),
CONSTRAINT fk_repositories_owner FOREIGN KEY (owner_user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_repositories_fork FOREIGN KEY (forked_from_repository_id) REFERENCES repositories(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
`CREATE TABLE IF NOT EXISTS repository_collaborators (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
repository_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
role ENUM('read','write') NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uniq_repo_user (repository_id, user_id),
CONSTRAINT fk_collab_repo FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE,
CONSTRAINT fk_collab_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
`CREATE TABLE IF NOT EXISTS pull_requests (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
target_repository_id BIGINT NOT NULL,
number INT NOT NULL,
author_user_id BIGINT NOT NULL,
source_repository_id BIGINT NOT NULL,
source_branch VARCHAR(200) NOT NULL,
target_branch VARCHAR(200) NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
status ENUM('open','closed','merged') NOT NULL DEFAULT 'open',
closed_at TIMESTAMP NULL,
merged_at TIMESTAMP NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uniq_target_number (target_repository_id, number),
INDEX idx_pr_target (target_repository_id),
INDEX idx_pr_source (source_repository_id),
CONSTRAINT fk_pr_target FOREIGN KEY (target_repository_id) REFERENCES repositories(id) ON DELETE CASCADE,
CONSTRAINT fk_pr_source FOREIGN KEY (source_repository_id) REFERENCES repositories(id) ON DELETE CASCADE,
CONSTRAINT fk_pr_author FOREIGN KEY (author_user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
`CREATE TABLE IF NOT EXISTS pull_request_comments (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
pull_request_id BIGINT NOT NULL,
author_user_id BIGINT NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_pr_comments_pr (pull_request_id),
CONSTRAINT fk_pr_comments_pr FOREIGN KEY (pull_request_id) REFERENCES pull_requests(id) ON DELETE CASCADE,
CONSTRAINT fk_pr_comments_author FOREIGN KEY (author_user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
}
for _, stmt := range stmts {
if _, err := db.Exec(stmt); err != nil {
return err
}
}
alterations := []string{
`ALTER TABLE users ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT false`,
`ALTER TABLE repositories ADD COLUMN description TEXT NULL`,
`ALTER TABLE repositories ADD COLUMN default_branch VARCHAR(200) NOT NULL DEFAULT 'main'`,
`ALTER TABLE repositories ADD COLUMN archived BOOLEAN NOT NULL DEFAULT false`,
}
for _, stmt := range alterations {
if _, err := db.Exec(stmt); err != nil && !isDuplicateColumnError(err) {
return err
}
}
return nil
}