mirror of
https://github.com/go-gitea/gitea
synced 2024-09-24 12:46:14 +00:00
Merge branch 'main' into xfo
This commit is contained in:
commit
608b6e01c7
286
cmd/dump.go
286
cmd/dump.go
@ -6,14 +6,13 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/modules/dump"
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
@ -25,89 +24,17 @@ import (
|
|||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func addReader(w archiver.Writer, r io.ReadCloser, info os.FileInfo, customName string, verbose bool) error {
|
|
||||||
if verbose {
|
|
||||||
log.Info("Adding file %s", customName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return w.Write(archiver.File{
|
|
||||||
FileInfo: archiver.FileInfo{
|
|
||||||
FileInfo: info,
|
|
||||||
CustomName: customName,
|
|
||||||
},
|
|
||||||
ReadCloser: r,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func addFile(w archiver.Writer, filePath, absPath string, verbose bool) error {
|
|
||||||
file, err := os.Open(absPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
fileInfo, err := file.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return addReader(w, file, fileInfo, filePath, verbose)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSubdir(upper, lower string) (bool, error) {
|
|
||||||
if relPath, err := filepath.Rel(upper, lower); err != nil {
|
|
||||||
return false, err
|
|
||||||
} else if relPath == "." || !strings.HasPrefix(relPath, ".") {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type outputType struct {
|
|
||||||
Enum []string
|
|
||||||
Default string
|
|
||||||
selected string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o outputType) Join() string {
|
|
||||||
return strings.Join(o.Enum, ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *outputType) Set(value string) error {
|
|
||||||
for _, enum := range o.Enum {
|
|
||||||
if enum == value {
|
|
||||||
o.selected = value
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("allowed values are %s", o.Join())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o outputType) String() string {
|
|
||||||
if o.selected == "" {
|
|
||||||
return o.Default
|
|
||||||
}
|
|
||||||
return o.selected
|
|
||||||
}
|
|
||||||
|
|
||||||
var outputTypeEnum = &outputType{
|
|
||||||
Enum: []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"},
|
|
||||||
Default: "zip",
|
|
||||||
}
|
|
||||||
|
|
||||||
// CmdDump represents the available dump sub-command.
|
// CmdDump represents the available dump sub-command.
|
||||||
var CmdDump = &cli.Command{
|
var CmdDump = &cli.Command{
|
||||||
Name: "dump",
|
Name: "dump",
|
||||||
Usage: "Dump Gitea files and database",
|
Usage: "Dump Gitea files and database",
|
||||||
Description: `Dump compresses all related files and database into zip file.
|
Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`,
|
||||||
It can be used for backup and capture Gitea server image to send to maintainer`,
|
|
||||||
Action: runDump,
|
Action: runDump,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "file",
|
Name: "file",
|
||||||
Aliases: []string{"f"},
|
Aliases: []string{"f"},
|
||||||
Value: fmt.Sprintf("gitea-dump-%d.zip", time.Now().Unix()),
|
Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`,
|
||||||
Usage: "Name of the dump file which will be created. Supply '-' for stdout. See type for available types.",
|
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "verbose",
|
Name: "verbose",
|
||||||
@ -160,64 +87,52 @@ It can be used for backup and capture Gitea server image to send to maintainer`,
|
|||||||
Name: "skip-index",
|
Name: "skip-index",
|
||||||
Usage: "Skip bleve index data",
|
Usage: "Skip bleve index data",
|
||||||
},
|
},
|
||||||
&cli.GenericFlag{
|
&cli.StringFlag{
|
||||||
Name: "type",
|
Name: "type",
|
||||||
Value: outputTypeEnum,
|
Usage: fmt.Sprintf(`Dump output format, default to "zip", supported types: %s`, strings.Join(dump.SupportedOutputTypes, ", ")),
|
||||||
Usage: fmt.Sprintf("Dump output format: %s", outputTypeEnum.Join()),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func fatal(format string, args ...any) {
|
func fatal(format string, args ...any) {
|
||||||
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
|
||||||
log.Fatal(format, args...)
|
log.Fatal(format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDump(ctx *cli.Context) error {
|
func runDump(ctx *cli.Context) error {
|
||||||
var file *os.File
|
|
||||||
fileName := ctx.String("file")
|
|
||||||
outType := ctx.String("type")
|
|
||||||
if fileName == "-" {
|
|
||||||
file = os.Stdout
|
|
||||||
setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)
|
|
||||||
} else {
|
|
||||||
for _, suffix := range outputTypeEnum.Enum {
|
|
||||||
if strings.HasSuffix(fileName, "."+suffix) {
|
|
||||||
fileName = strings.TrimSuffix(fileName, "."+suffix)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fileName += "." + outType
|
|
||||||
}
|
|
||||||
setting.MustInstalled()
|
setting.MustInstalled()
|
||||||
|
|
||||||
// make sure we are logging to the console no matter what the configuration tells us do to
|
quite := ctx.Bool("quiet")
|
||||||
// FIXME: don't use CfgProvider directly
|
|
||||||
if _, err := setting.CfgProvider.Section("log").NewKey("MODE", "console"); err != nil {
|
|
||||||
fatal("Setting logging mode to console failed: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := setting.CfgProvider.Section("log.console").NewKey("STDERR", "true"); err != nil {
|
|
||||||
fatal("Setting console logger to stderr failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set loglevel to Warn if quiet-mode is requested
|
|
||||||
if ctx.Bool("quiet") {
|
|
||||||
if _, err := setting.CfgProvider.Section("log.console").NewKey("LEVEL", "Warn"); err != nil {
|
|
||||||
fatal("Setting console log-level failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !setting.InstallLock {
|
|
||||||
log.Error("Is '%s' really the right config path?\n", setting.CustomConf)
|
|
||||||
return fmt.Errorf("gitea is not initialized")
|
|
||||||
}
|
|
||||||
setting.LoadSettings() // cannot access session settings otherwise
|
|
||||||
|
|
||||||
verbose := ctx.Bool("verbose")
|
verbose := ctx.Bool("verbose")
|
||||||
if verbose && ctx.Bool("quiet") {
|
if verbose && quite {
|
||||||
return fmt.Errorf("--quiet and --verbose cannot both be set")
|
fatal("Option --quiet and --verbose cannot both be set")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// outFileName is either "-" or a file name (will be made absolute)
|
||||||
|
outFileName, outType := dump.PrepareFileNameAndType(ctx.String("file"), ctx.String("type"))
|
||||||
|
if outType == "" {
|
||||||
|
fatal("Invalid output type")
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile := os.Stdout
|
||||||
|
if outFileName != "-" {
|
||||||
|
var err error
|
||||||
|
if outFileName, err = filepath.Abs(outFileName); err != nil {
|
||||||
|
fatal("Unable to get absolute path of dump file: %v", err)
|
||||||
|
}
|
||||||
|
if exist, _ := util.IsExist(outFileName); exist {
|
||||||
|
fatal("Dump file %q exists", outFileName)
|
||||||
|
}
|
||||||
|
if outFile, err = os.Create(outFileName); err != nil {
|
||||||
|
fatal("Unable to create dump file %q: %v", outFileName, err)
|
||||||
|
}
|
||||||
|
defer outFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
setupConsoleLogger(util.Iif(quite, log.WARN, log.INFO), log.CanColorStderr, os.Stderr)
|
||||||
|
|
||||||
|
setting.DisableLoggerInit()
|
||||||
|
setting.LoadSettings() // cannot access session settings otherwise
|
||||||
|
|
||||||
stdCtx, cancel := installSignals()
|
stdCtx, cancel := installSignals()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -226,44 +141,32 @@ func runDump(ctx *cli.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := storage.Init(); err != nil {
|
if err = storage.Init(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if file == nil {
|
archiverGeneric, err := archiver.ByExtension("." + outType)
|
||||||
file, err = os.Create(fileName)
|
|
||||||
if err != nil {
|
|
||||||
fatal("Unable to open %s: %v", fileName, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
absFileName, err := filepath.Abs(fileName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var iface any
|
|
||||||
if fileName == "-" {
|
|
||||||
iface, err = archiver.ByExtension(fmt.Sprintf(".%s", outType))
|
|
||||||
} else {
|
|
||||||
iface, err = archiver.ByExtension(fileName)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatal("Unable to get archiver for extension: %v", err)
|
fatal("Unable to get archiver for extension: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
w, _ := iface.(archiver.Writer)
|
archiverWriter := archiverGeneric.(archiver.Writer)
|
||||||
if err := w.Create(file); err != nil {
|
if err := archiverWriter.Create(outFile); err != nil {
|
||||||
fatal("Creating archiver.Writer failed: %v", err)
|
fatal("Creating archiver.Writer failed: %v", err)
|
||||||
}
|
}
|
||||||
defer w.Close()
|
defer archiverWriter.Close()
|
||||||
|
|
||||||
|
dumper := &dump.Dumper{
|
||||||
|
Writer: archiverWriter,
|
||||||
|
Verbose: verbose,
|
||||||
|
}
|
||||||
|
dumper.GlobalExcludeAbsPath(outFileName)
|
||||||
|
|
||||||
if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") {
|
if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") {
|
||||||
log.Info("Skip dumping local repositories")
|
log.Info("Skip dumping local repositories")
|
||||||
} else {
|
} else {
|
||||||
log.Info("Dumping local repositories... %s", setting.RepoRootPath)
|
log.Info("Dumping local repositories... %s", setting.RepoRootPath)
|
||||||
if err := addRecursiveExclude(w, "repos", setting.RepoRootPath, []string{absFileName}, verbose); err != nil {
|
if err := dumper.AddRecursiveExclude("repos", setting.RepoRootPath, nil); err != nil {
|
||||||
fatal("Failed to include repositories: %v", err)
|
fatal("Failed to include repositories: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,8 +179,7 @@ func runDump(ctx *cli.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return dumper.AddReader(object, info, path.Join("data", "lfs", objPath))
|
||||||
return addReader(w, object, info, path.Join("data", "lfs", objPath), verbose)
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
fatal("Failed to dump LFS objects: %v", err)
|
fatal("Failed to dump LFS objects: %v", err)
|
||||||
}
|
}
|
||||||
@ -310,24 +212,22 @@ func runDump(ctx *cli.Context) error {
|
|||||||
fatal("Failed to dump database: %v", err)
|
fatal("Failed to dump database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := addFile(w, "gitea-db.sql", dbDump.Name(), verbose); err != nil {
|
if err = dumper.AddFile("gitea-db.sql", dbDump.Name()); err != nil {
|
||||||
fatal("Failed to include gitea-db.sql: %v", err)
|
fatal("Failed to include gitea-db.sql: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(setting.CustomConf) > 0 {
|
|
||||||
log.Info("Adding custom configuration file from %s", setting.CustomConf)
|
log.Info("Adding custom configuration file from %s", setting.CustomConf)
|
||||||
if err := addFile(w, "app.ini", setting.CustomConf, verbose); err != nil {
|
if err = dumper.AddFile("app.ini", setting.CustomConf); err != nil {
|
||||||
fatal("Failed to include specified app.ini: %v", err)
|
fatal("Failed to include specified app.ini: %v", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") {
|
if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") {
|
||||||
log.Info("Skipping custom directory")
|
log.Info("Skipping custom directory")
|
||||||
} else {
|
} else {
|
||||||
customDir, err := os.Stat(setting.CustomPath)
|
customDir, err := os.Stat(setting.CustomPath)
|
||||||
if err == nil && customDir.IsDir() {
|
if err == nil && customDir.IsDir() {
|
||||||
if is, _ := isSubdir(setting.AppDataPath, setting.CustomPath); !is {
|
if is, _ := dump.IsSubdir(setting.AppDataPath, setting.CustomPath); !is {
|
||||||
if err := addRecursiveExclude(w, "custom", setting.CustomPath, []string{absFileName}, verbose); err != nil {
|
if err := dumper.AddRecursiveExclude("custom", setting.CustomPath, nil); err != nil {
|
||||||
fatal("Failed to include custom: %v", err)
|
fatal("Failed to include custom: %v", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -364,8 +264,7 @@ func runDump(ctx *cli.Context) error {
|
|||||||
excludes = append(excludes, setting.Attachment.Storage.Path)
|
excludes = append(excludes, setting.Attachment.Storage.Path)
|
||||||
excludes = append(excludes, setting.Packages.Storage.Path)
|
excludes = append(excludes, setting.Packages.Storage.Path)
|
||||||
excludes = append(excludes, setting.Log.RootPath)
|
excludes = append(excludes, setting.Log.RootPath)
|
||||||
excludes = append(excludes, absFileName)
|
if err := dumper.AddRecursiveExclude("data", setting.AppDataPath, excludes); err != nil {
|
||||||
if err := addRecursiveExclude(w, "data", setting.AppDataPath, excludes, verbose); err != nil {
|
|
||||||
fatal("Failed to include data directory: %v", err)
|
fatal("Failed to include data directory: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -377,8 +276,7 @@ func runDump(ctx *cli.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return dumper.AddReader(object, info, path.Join("data", "attachments", objPath))
|
||||||
return addReader(w, object, info, path.Join("data", "attachments", objPath), verbose)
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
fatal("Failed to dump attachments: %v", err)
|
fatal("Failed to dump attachments: %v", err)
|
||||||
}
|
}
|
||||||
@ -392,8 +290,7 @@ func runDump(ctx *cli.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return dumper.AddReader(object, info, path.Join("data", "packages", objPath))
|
||||||
return addReader(w, object, info, path.Join("data", "packages", objPath), verbose)
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
fatal("Failed to dump packages: %v", err)
|
fatal("Failed to dump packages: %v", err)
|
||||||
}
|
}
|
||||||
@ -409,80 +306,23 @@ func runDump(ctx *cli.Context) error {
|
|||||||
log.Error("Unable to check if %s exists. Error: %v", setting.Log.RootPath, err)
|
log.Error("Unable to check if %s exists. Error: %v", setting.Log.RootPath, err)
|
||||||
}
|
}
|
||||||
if isExist {
|
if isExist {
|
||||||
if err := addRecursiveExclude(w, "log", setting.Log.RootPath, []string{absFileName}, verbose); err != nil {
|
if err := dumper.AddRecursiveExclude("log", setting.Log.RootPath, nil); err != nil {
|
||||||
fatal("Failed to include log: %v", err)
|
fatal("Failed to include log: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if fileName != "-" {
|
if outFileName == "-" {
|
||||||
if err = w.Close(); err != nil {
|
log.Info("Finish dumping to stdout")
|
||||||
_ = util.Remove(fileName)
|
} else {
|
||||||
fatal("Failed to save %s: %v", fileName, err)
|
if err = archiverWriter.Close(); err != nil {
|
||||||
|
_ = os.Remove(outFileName)
|
||||||
|
fatal("Failed to save %q: %v", outFileName, err)
|
||||||
}
|
}
|
||||||
|
if err = os.Chmod(outFileName, 0o600); err != nil {
|
||||||
if err := os.Chmod(fileName, 0o600); err != nil {
|
|
||||||
log.Info("Can't change file access permissions mask to 0600: %v", err)
|
log.Info("Can't change file access permissions mask to 0600: %v", err)
|
||||||
}
|
}
|
||||||
}
|
log.Info("Finish dumping in file %s", outFileName)
|
||||||
|
|
||||||
if fileName != "-" {
|
|
||||||
log.Info("Finish dumping in file %s", fileName)
|
|
||||||
} else {
|
|
||||||
log.Info("Finish dumping to stdout")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// addRecursiveExclude zips absPath to specified insidePath inside writer excluding excludeAbsPath
|
|
||||||
func addRecursiveExclude(w archiver.Writer, insidePath, absPath string, excludeAbsPath []string, verbose bool) error {
|
|
||||||
absPath, err := filepath.Abs(absPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dir, err := os.Open(absPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer dir.Close()
|
|
||||||
|
|
||||||
files, err := dir.Readdir(0)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, file := range files {
|
|
||||||
currentAbsPath := filepath.Join(absPath, file.Name())
|
|
||||||
currentInsidePath := path.Join(insidePath, file.Name())
|
|
||||||
if file.IsDir() {
|
|
||||||
if !util.SliceContainsString(excludeAbsPath, currentAbsPath) {
|
|
||||||
if err := addFile(w, currentInsidePath, currentAbsPath, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = addRecursiveExclude(w, currentInsidePath, currentAbsPath, excludeAbsPath, verbose); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// only copy regular files and symlink regular files, skip non-regular files like socket/pipe/...
|
|
||||||
shouldAdd := file.Mode().IsRegular()
|
|
||||||
if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink {
|
|
||||||
target, err := filepath.EvalSymlinks(currentAbsPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
targetStat, err := os.Stat(target)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
shouldAdd = targetStat.Mode().IsRegular()
|
|
||||||
}
|
|
||||||
if shouldAdd {
|
|
||||||
if err = addFile(w, currentInsidePath, currentAbsPath, verbose); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -545,7 +545,7 @@ In this option, the idea is that the host SSH uses an `AuthorizedKeysCommand` in
|
|||||||
```bash
|
```bash
|
||||||
cat <<"EOF" | sudo tee /home/git/docker-shell
|
cat <<"EOF" | sudo tee /home/git/docker-shell
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
/usr/bin/docker exec -i --env SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" gitea sh "$@"
|
/usr/bin/docker exec -i -u git --env SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" gitea sh "$@"
|
||||||
EOF
|
EOF
|
||||||
sudo chmod +x /home/git/docker-shell
|
sudo chmod +x /home/git/docker-shell
|
||||||
sudo usermod -s /home/git/docker-shell git
|
sudo usermod -s /home/git/docker-shell git
|
||||||
@ -560,7 +560,7 @@ Add the following block to `/etc/ssh/sshd_config`, on the host:
|
|||||||
```bash
|
```bash
|
||||||
Match User git
|
Match User git
|
||||||
AuthorizedKeysCommandUser git
|
AuthorizedKeysCommandUser git
|
||||||
AuthorizedKeysCommand /usr/bin/docker exec -i gitea /usr/local/bin/gitea keys -c /data/gitea/conf/app.ini -e git -u %u -t %t -k %k
|
AuthorizedKeysCommand /usr/bin/docker exec -i -u git gitea /usr/local/bin/gitea keys -c /data/gitea/conf/app.ini -e git -u %u -t %t -k %k
|
||||||
```
|
```
|
||||||
|
|
||||||
(From 1.16.0 you will not need to set the `-c /data/gitea/conf/app.ini` option.)
|
(From 1.16.0 you will not need to set the `-c /data/gitea/conf/app.ini` option.)
|
||||||
|
174
modules/dump/dumper.go
Normal file
174
modules/dump/dumper.go
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package dump
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"github.com/mholt/archiver/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var SupportedOutputTypes = []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"}
|
||||||
|
|
||||||
|
// PrepareFileNameAndType prepares the output file name and type, if the type is not supported, it returns an empty "outType"
|
||||||
|
func PrepareFileNameAndType(argFile, argType string) (outFileName, outType string) {
|
||||||
|
if argFile == "" && argType == "" {
|
||||||
|
outType = SupportedOutputTypes[0]
|
||||||
|
outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType)
|
||||||
|
} else if argFile == "" {
|
||||||
|
outType = argType
|
||||||
|
outFileName = fmt.Sprintf("gitea-dump-%d.%s", timeutil.TimeStampNow(), outType)
|
||||||
|
} else if argType == "" {
|
||||||
|
if filepath.Ext(outFileName) == "" {
|
||||||
|
outType = SupportedOutputTypes[0]
|
||||||
|
outFileName = argFile
|
||||||
|
} else {
|
||||||
|
for _, t := range SupportedOutputTypes {
|
||||||
|
if strings.HasSuffix(argFile, "."+t) {
|
||||||
|
outFileName = argFile
|
||||||
|
outType = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outFileName, outType = argFile, argType
|
||||||
|
}
|
||||||
|
if !slices.Contains(SupportedOutputTypes, outType) {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
return outFileName, outType
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsSubdir(upper, lower string) (bool, error) {
|
||||||
|
if relPath, err := filepath.Rel(upper, lower); err != nil {
|
||||||
|
return false, err
|
||||||
|
} else if relPath == "." || !strings.HasPrefix(relPath, ".") {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Dumper struct {
|
||||||
|
Writer archiver.Writer
|
||||||
|
Verbose bool
|
||||||
|
|
||||||
|
globalExcludeAbsPaths []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dumper *Dumper) AddReader(r io.ReadCloser, info os.FileInfo, customName string) error {
|
||||||
|
if dumper.Verbose {
|
||||||
|
log.Info("Adding file %s", customName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dumper.Writer.Write(archiver.File{
|
||||||
|
FileInfo: archiver.FileInfo{
|
||||||
|
FileInfo: info,
|
||||||
|
CustomName: customName,
|
||||||
|
},
|
||||||
|
ReadCloser: r,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dumper *Dumper) AddFile(filePath, absPath string) error {
|
||||||
|
file, err := os.Open(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
fileInfo, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return dumper.AddReader(file, fileInfo, filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dumper *Dumper) normalizeFilePath(absPath string) string {
|
||||||
|
absPath = filepath.Clean(absPath)
|
||||||
|
if setting.IsWindows {
|
||||||
|
absPath = strings.ToLower(absPath)
|
||||||
|
}
|
||||||
|
return absPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dumper *Dumper) GlobalExcludeAbsPath(absPaths ...string) {
|
||||||
|
for _, absPath := range absPaths {
|
||||||
|
dumper.globalExcludeAbsPaths = append(dumper.globalExcludeAbsPaths, dumper.normalizeFilePath(absPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dumper *Dumper) shouldExclude(absPath string, excludes []string) bool {
|
||||||
|
norm := dumper.normalizeFilePath(absPath)
|
||||||
|
return slices.Contains(dumper.globalExcludeAbsPaths, norm) || slices.Contains(excludes, norm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dumper *Dumper) AddRecursiveExclude(insidePath, absPath string, excludes []string) error {
|
||||||
|
excludes = slices.Clone(excludes)
|
||||||
|
for i := range excludes {
|
||||||
|
excludes[i] = dumper.normalizeFilePath(excludes[i])
|
||||||
|
}
|
||||||
|
return dumper.addFileOrDir(insidePath, absPath, excludes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dumper *Dumper) addFileOrDir(insidePath, absPath string, excludes []string) error {
|
||||||
|
absPath, err := filepath.Abs(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dir, err := os.Open(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dir.Close()
|
||||||
|
|
||||||
|
files, err := dir.Readdir(0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
currentAbsPath := filepath.Join(absPath, file.Name())
|
||||||
|
if dumper.shouldExclude(currentAbsPath, excludes) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
currentInsidePath := path.Join(insidePath, file.Name())
|
||||||
|
if file.IsDir() {
|
||||||
|
if err := dumper.AddFile(currentInsidePath, currentAbsPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = dumper.addFileOrDir(currentInsidePath, currentAbsPath, excludes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// only copy regular files and symlink regular files, skip non-regular files like socket/pipe/...
|
||||||
|
shouldAdd := file.Mode().IsRegular()
|
||||||
|
if !shouldAdd && file.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||||
|
target, err := filepath.EvalSymlinks(currentAbsPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
targetStat, err := os.Stat(target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
shouldAdd = targetStat.Mode().IsRegular()
|
||||||
|
}
|
||||||
|
if shouldAdd {
|
||||||
|
if err = dumper.AddFile(currentInsidePath, currentAbsPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
113
modules/dump/dumper_test.go
Normal file
113
modules/dump/dumper_test.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package dump
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"github.com/mholt/archiver/v3"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPrepareFileNameAndType(t *testing.T) {
|
||||||
|
defer timeutil.MockSet(time.Unix(1234, 0))()
|
||||||
|
test := func(argFile, argType, expFile, expType string) {
|
||||||
|
outFile, outType := PrepareFileNameAndType(argFile, argType)
|
||||||
|
assert.Equal(t,
|
||||||
|
fmt.Sprintf("outFile=%s, outType=%s", expFile, expType),
|
||||||
|
fmt.Sprintf("outFile=%s, outType=%s", outFile, outType),
|
||||||
|
fmt.Sprintf("argFile=%s, argType=%s", argFile, argType),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("", "", "gitea-dump-1234.zip", "zip")
|
||||||
|
test("", "tar.gz", "gitea-dump-1234.tar.gz", "tar.gz")
|
||||||
|
test("", "no-such", "", "")
|
||||||
|
|
||||||
|
test("-", "", "-", "zip")
|
||||||
|
test("-", "tar.gz", "-", "tar.gz")
|
||||||
|
test("-", "no-such", "", "")
|
||||||
|
|
||||||
|
test("a", "", "a", "zip")
|
||||||
|
test("a", "tar.gz", "a", "tar.gz")
|
||||||
|
test("a", "no-such", "", "")
|
||||||
|
|
||||||
|
test("a.zip", "", "a.zip", "zip")
|
||||||
|
test("a.zip", "tar.gz", "a.zip", "tar.gz")
|
||||||
|
test("a.zip", "no-such", "", "")
|
||||||
|
|
||||||
|
test("a.tar.gz", "", "a.tar.gz", "zip")
|
||||||
|
test("a.tar.gz", "tar.gz", "a.tar.gz", "tar.gz")
|
||||||
|
test("a.tar.gz", "no-such", "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsSubDir(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
_ = os.MkdirAll(filepath.Join(tmpDir, "include/sub"), 0o755)
|
||||||
|
|
||||||
|
isSub, err := IsSubdir(filepath.Join(tmpDir, "include"), filepath.Join(tmpDir, "include"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, isSub)
|
||||||
|
|
||||||
|
isSub, err = IsSubdir(filepath.Join(tmpDir, "include"), filepath.Join(tmpDir, "include/sub"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, isSub)
|
||||||
|
|
||||||
|
isSub, err = IsSubdir(filepath.Join(tmpDir, "include/sub"), filepath.Join(tmpDir, "include"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, isSub)
|
||||||
|
}
|
||||||
|
|
||||||
|
type testWriter struct {
|
||||||
|
added []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testWriter) Create(out io.Writer) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testWriter) Write(f archiver.File) error {
|
||||||
|
t.added = append(t.added, f.Name())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testWriter) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDumper(t *testing.T) {
|
||||||
|
sortStrings := func(s []string) []string {
|
||||||
|
sort.Strings(s)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
_ = os.MkdirAll(filepath.Join(tmpDir, "include/exclude1"), 0o755)
|
||||||
|
_ = os.MkdirAll(filepath.Join(tmpDir, "include/exclude2"), 0o755)
|
||||||
|
_ = os.MkdirAll(filepath.Join(tmpDir, "include/sub"), 0o755)
|
||||||
|
_ = os.WriteFile(filepath.Join(tmpDir, "include/a"), nil, 0o644)
|
||||||
|
_ = os.WriteFile(filepath.Join(tmpDir, "include/sub/b"), nil, 0o644)
|
||||||
|
_ = os.WriteFile(filepath.Join(tmpDir, "include/exclude1/a-1"), nil, 0o644)
|
||||||
|
_ = os.WriteFile(filepath.Join(tmpDir, "include/exclude2/a-2"), nil, 0o644)
|
||||||
|
|
||||||
|
tw := &testWriter{}
|
||||||
|
d := &Dumper{Writer: tw}
|
||||||
|
d.GlobalExcludeAbsPath(filepath.Join(tmpDir, "include/exclude1"))
|
||||||
|
err := d.AddRecursiveExclude("include", filepath.Join(tmpDir, "include"), []string{filepath.Join(tmpDir, "include/exclude2")})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, sortStrings([]string{"include/a", "include/sub", "include/sub/b"}), sortStrings(tw.added))
|
||||||
|
|
||||||
|
tw = &testWriter{}
|
||||||
|
d = &Dumper{Writer: tw}
|
||||||
|
err = d.AddRecursiveExclude("include", filepath.Join(tmpDir, "include"), nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, sortStrings([]string{"include/exclude2", "include/exclude2/a-2", "include/a", "include/sub", "include/sub/b", "include/exclude1", "include/exclude1/a-1"}), sortStrings(tw.added))
|
||||||
|
}
|
@ -185,8 +185,13 @@ func InitLoggersForTest() {
|
|||||||
initAllLoggers()
|
initAllLoggers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var initLoggerDisabled bool
|
||||||
|
|
||||||
// initAllLoggers creates all the log services
|
// initAllLoggers creates all the log services
|
||||||
func initAllLoggers() {
|
func initAllLoggers() {
|
||||||
|
if initLoggerDisabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
initManagedLoggers(log.GetManager(), CfgProvider)
|
initManagedLoggers(log.GetManager(), CfgProvider)
|
||||||
|
|
||||||
golog.SetFlags(0)
|
golog.SetFlags(0)
|
||||||
@ -194,6 +199,10 @@ func initAllLoggers() {
|
|||||||
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
|
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DisableLoggerInit() {
|
||||||
|
initLoggerDisabled = true
|
||||||
|
}
|
||||||
|
|
||||||
func initManagedLoggers(manager *log.LoggerManager, cfg ConfigProvider) {
|
func initManagedLoggers(manager *log.LoggerManager, cfg ConfigProvider) {
|
||||||
loadLogGlobalFrom(cfg)
|
loadLogGlobalFrom(cfg)
|
||||||
prepareLoggerConfig(cfg)
|
prepareLoggerConfig(cfg)
|
||||||
|
@ -21,8 +21,9 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// MockSet sets the time to a mocked time.Time
|
// MockSet sets the time to a mocked time.Time
|
||||||
func MockSet(now time.Time) {
|
func MockSet(now time.Time) func() {
|
||||||
mockNow = now
|
mockNow = now
|
||||||
|
return MockUnset
|
||||||
}
|
}
|
||||||
|
|
||||||
// MockUnset will unset the mocked time.Time
|
// MockUnset will unset the mocked time.Time
|
||||||
|
@ -213,6 +213,14 @@ func ToPointer[T any](val T) *T {
|
|||||||
return &val
|
return &val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Iif is an "inline-if", it returns "trueVal" if "condition" is true, otherwise "falseVal"
|
||||||
|
func Iif[T comparable](condition bool, trueVal, falseVal T) T {
|
||||||
|
if condition {
|
||||||
|
return trueVal
|
||||||
|
}
|
||||||
|
return falseVal
|
||||||
|
}
|
||||||
|
|
||||||
// IfZero returns "def" if "v" is a zero value, otherwise "v"
|
// IfZero returns "def" if "v" is a zero value, otherwise "v"
|
||||||
func IfZero[T comparable](v, def T) T {
|
func IfZero[T comparable](v, def T) T {
|
||||||
var zero T
|
var zero T
|
||||||
|
12
package-lock.json
generated
12
package-lock.json
generated
@ -13,7 +13,6 @@
|
|||||||
"@github/relative-time-element": "4.4.0",
|
"@github/relative-time-element": "4.4.0",
|
||||||
"@github/text-expander-element": "2.6.1",
|
"@github/text-expander-element": "2.6.1",
|
||||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||||
"@melloware/coloris": "0.23.0",
|
|
||||||
"@primer/octicons": "19.9.0",
|
"@primer/octicons": "19.9.0",
|
||||||
"add-asset-webpack-plugin": "2.0.1",
|
"add-asset-webpack-plugin": "2.0.1",
|
||||||
"ansi_up": "6.0.2",
|
"ansi_up": "6.0.2",
|
||||||
@ -54,6 +53,7 @@
|
|||||||
"toastify-js": "1.12.0",
|
"toastify-js": "1.12.0",
|
||||||
"tributejs": "5.1.3",
|
"tributejs": "5.1.3",
|
||||||
"uint8-to-base64": "0.2.0",
|
"uint8-to-base64": "0.2.0",
|
||||||
|
"vanilla-colorful": "0.7.2",
|
||||||
"vue": "3.4.21",
|
"vue": "3.4.21",
|
||||||
"vue-bar-graph": "2.0.0",
|
"vue-bar-graph": "2.0.0",
|
||||||
"vue-chartjs": "5.3.0",
|
"vue-chartjs": "5.3.0",
|
||||||
@ -1290,11 +1290,6 @@
|
|||||||
"@mcaptcha/core-glue": "^0.1.0-alpha-5"
|
"@mcaptcha/core-glue": "^0.1.0-alpha-5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@melloware/coloris": {
|
|
||||||
"version": "0.23.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@melloware/coloris/-/coloris-0.23.0.tgz",
|
|
||||||
"integrity": "sha512-VGIjI9+IQwg6BHjIE10yl0K2ARYz5bsjn6BgFEs1y1ErPAQymgdoxwVcSVL4Ai5t9OVs8xaCB7JKHqFu2N96Ow=="
|
|
||||||
},
|
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@ -11853,6 +11848,11 @@
|
|||||||
"builtins": "^1.0.3"
|
"builtins": "^1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vanilla-colorful": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vanilla-colorful/-/vanilla-colorful-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-z2YZusTFC6KnLERx1cgoIRX2CjPRP0W75N+3CC6gbvdX5Ch47rZkEMGO2Xnf+IEmi3RiFLxS18gayMA27iU7Kg=="
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.2.6",
|
"version": "5.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz",
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
"@github/relative-time-element": "4.4.0",
|
"@github/relative-time-element": "4.4.0",
|
||||||
"@github/text-expander-element": "2.6.1",
|
"@github/text-expander-element": "2.6.1",
|
||||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||||
"@melloware/coloris": "0.23.0",
|
|
||||||
"@primer/octicons": "19.9.0",
|
"@primer/octicons": "19.9.0",
|
||||||
"add-asset-webpack-plugin": "2.0.1",
|
"add-asset-webpack-plugin": "2.0.1",
|
||||||
"ansi_up": "6.0.2",
|
"ansi_up": "6.0.2",
|
||||||
@ -53,6 +52,7 @@
|
|||||||
"toastify-js": "1.12.0",
|
"toastify-js": "1.12.0",
|
||||||
"tributejs": "5.1.3",
|
"tributejs": "5.1.3",
|
||||||
"uint8-to-base64": "0.2.0",
|
"uint8-to-base64": "0.2.0",
|
||||||
|
"vanilla-colorful": "0.7.2",
|
||||||
"vue": "3.4.21",
|
"vue": "3.4.21",
|
||||||
"vue-bar-graph": "2.0.0",
|
"vue-bar-graph": "2.0.0",
|
||||||
"vue-chartjs": "5.3.0",
|
"vue-chartjs": "5.3.0",
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
/* This is a stripped-down version of coloris's CSS tailored to our needs. It does only include
|
|
||||||
opaqua colors, and if more features like opacity are needed, the CSS needs to be extended
|
|
||||||
based on upstream: https://github.com/mdbassit/Coloris/blob/main/src/coloris.css. */
|
|
||||||
|
|
||||||
.js-color-picker-input {
|
.js-color-picker-input {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.js-color-picker-input input {
|
.js-color-picker-input input {
|
||||||
@ -13,152 +9,39 @@
|
|||||||
padding-left: 32px !important;
|
padding-left: 32px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clr-picker {
|
.js-color-picker-input .preview-square {
|
||||||
display: none;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
position: absolute;
|
|
||||||
width: 200px;
|
|
||||||
z-index: 1002; /* above .ui.modal which has 1001 */
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
background-color: var(--color-menu);
|
|
||||||
justify-content: flex-end;
|
|
||||||
direction: ltr;
|
|
||||||
box-shadow: 0 5px 20px var(--color-shadow);
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clr-picker.clr-open {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clr-gradient {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
border-radius: 3px 3px 0 0;
|
|
||||||
background: linear-gradient(rgba(0,0,0,0), #000), linear-gradient(90deg, #fff, currentcolor); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clr-marker {
|
|
||||||
position: absolute;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
margin: -6px 0 0 -6px;
|
|
||||||
border: 1px solid var(--color-white);
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: currentcolor;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clr-picker input[type="range"]::-webkit-slider-runnable-track {
|
|
||||||
width: 100%;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clr-picker input[type="range"]::-webkit-slider-thumb {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clr-picker input[type="range"]::-moz-range-track {
|
|
||||||
width: 100%;
|
|
||||||
height: 16px;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clr-picker input[type="range"]::-moz-range-thumb {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clr-hue {
|
|
||||||
background: linear-gradient(to right, #f00 0%, #ff0 16.66%, #0f0 33.33%, #0ff 50%, #00f 66.66%, #f0f 83.33%, #f00 100%); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
|
|
||||||
position: relative;
|
|
||||||
width: calc(100% - 40px);
|
|
||||||
height: 10px;
|
|
||||||
margin: 10px 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clr-hue input[type="range"] {
|
|
||||||
position: absolute;
|
|
||||||
width: calc(100% + 32px);
|
|
||||||
margin: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
opacity: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clr-hue div {
|
|
||||||
position: absolute;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
left: 0;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
border: 2px solid var(--color-white);
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: currentcolor;
|
|
||||||
box-shadow: 0 0 1px var(--color-shadow);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clr-field {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clr-field button {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
color: inherit;
|
|
||||||
pointer-events: none;
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
|
background: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
|
||||||
background-position: 0 0, 4px 4px;
|
background-position: 0 0, 4px 4px;
|
||||||
background-size: 8px 8px;
|
background-size: 8px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clr-field button::after {
|
.js-color-picker-input .preview-square::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
background-color: currentcolor;
|
background-color: currentcolor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clr-marker:focus {
|
hex-color-picker {
|
||||||
outline: none;
|
width: 180px;
|
||||||
|
height: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clr-keyboard-nav .clr-marker:focus,
|
hex-color-picker::part(hue-pointer),
|
||||||
.clr-keyboard-nav .clr-hue input:focus + div,
|
hex-color-picker::part(saturation-pointer) {
|
||||||
.clr-keyboard-nav .clr-alpha input:focus + div {
|
width: 22px;
|
||||||
outline: none;
|
height: 22px;
|
||||||
box-shadow: 0 0 2px 2px var(--color-white);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.clr-picker .clr-preview,
|
hex-color-picker::part(hue) {
|
||||||
.clr-picker .clr-clear,
|
flex-basis: 16px;
|
||||||
.clr-picker .clr-swatches,
|
|
||||||
.clr-picker .clr-format,
|
|
||||||
.clr-picker .clr-alpha,
|
|
||||||
.clr-picker .clr-color {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
.markup .code-preview-container table {
|
.markup .code-preview-container table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 100px;
|
max-height: 240px; /* 12 lines at 20px per line */
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
margin: 0; /* override ".markup table {margin}" */
|
margin: 0; /* override ".markup table {margin}" */
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,17 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* bare theme, no styling at all, except box-shadow */
|
||||||
|
.tippy-box[data-theme="bare"] {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 6px 18px var(--color-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tippy-box[data-theme="bare"] .tippy-content {
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
/* tooltip theme for text tooltips */
|
/* tooltip theme for text tooltips */
|
||||||
|
|
||||||
.tippy-box[data-theme="tooltip"] {
|
.tippy-box[data-theme="tooltip"] {
|
||||||
|
@ -1,31 +1,66 @@
|
|||||||
export async function initColorPickers(selector = '.js-color-picker-input input', opts = {}) {
|
import {createTippy} from '../modules/tippy.js';
|
||||||
const inputEls = document.querySelectorAll(selector);
|
|
||||||
if (!inputEls.length) return;
|
|
||||||
|
|
||||||
const [{coloris, init}] = await Promise.all([
|
export async function initColorPickers() {
|
||||||
import(/* webpackChunkName: "colorpicker" */'@melloware/coloris'),
|
const els = document.getElementsByClassName('js-color-picker-input');
|
||||||
|
if (!els.length) return;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
import(/* webpackChunkName: "colorpicker" */'vanilla-colorful/hex-color-picker.js'),
|
||||||
import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'),
|
import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
init();
|
for (const el of els) {
|
||||||
coloris({
|
initPicker(el);
|
||||||
el: selector,
|
}
|
||||||
alpha: false,
|
}
|
||||||
focusInput: true,
|
|
||||||
selectInput: false,
|
function updateSquare(el, newValue) {
|
||||||
...opts,
|
el.style.color = /#[0-9a-f]{6}/i.test(newValue) ? newValue : 'transparent';
|
||||||
});
|
}
|
||||||
|
|
||||||
for (const inputEl of inputEls) {
|
function updatePicker(el, newValue) {
|
||||||
const parent = inputEl.closest('.js-color-picker-input');
|
el.setAttribute('color', newValue);
|
||||||
// prevent tabbing on the color preview `button` inside the input
|
}
|
||||||
parent.querySelector('button').tabIndex = -1;
|
|
||||||
// init precolors
|
function initPicker(el) {
|
||||||
for (const el of parent.querySelectorAll('.precolors .color')) {
|
const input = el.querySelector('input');
|
||||||
el.addEventListener('click', (e) => {
|
|
||||||
inputEl.value = e.target.getAttribute('data-color-hex');
|
const square = document.createElement('div');
|
||||||
inputEl.dispatchEvent(new Event('input', {bubbles: true}));
|
square.classList.add('preview-square');
|
||||||
});
|
updateSquare(square, input.value);
|
||||||
}
|
el.append(square);
|
||||||
|
|
||||||
|
const picker = document.createElement('hex-color-picker');
|
||||||
|
picker.addEventListener('color-changed', (e) => {
|
||||||
|
input.value = e.detail.value;
|
||||||
|
input.focus();
|
||||||
|
updateSquare(square, e.detail.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('input', (e) => {
|
||||||
|
updateSquare(square, e.target.value);
|
||||||
|
updatePicker(picker, e.target.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
createTippy(input, {
|
||||||
|
trigger: 'focus click',
|
||||||
|
theme: 'bare',
|
||||||
|
hideOnClick: true,
|
||||||
|
content: picker,
|
||||||
|
placement: 'bottom-start',
|
||||||
|
interactive: true,
|
||||||
|
onShow() {
|
||||||
|
updatePicker(picker, input.value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// init precolors
|
||||||
|
for (const colorEl of el.querySelectorAll('.precolors .color')) {
|
||||||
|
colorEl.addEventListener('click', (e) => {
|
||||||
|
const newValue = e.target.getAttribute('data-color-hex');
|
||||||
|
input.value = newValue;
|
||||||
|
input.dispatchEvent(new Event('input', {bubbles: true}));
|
||||||
|
updateSquare(square, newValue);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,12 @@ import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
|
|||||||
import {formatDatetime} from '../utils/time.js';
|
import {formatDatetime} from '../utils/time.js';
|
||||||
|
|
||||||
const visibleInstances = new Set();
|
const visibleInstances = new Set();
|
||||||
|
const arrowSvg = `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`;
|
||||||
|
|
||||||
export function createTippy(target, opts = {}) {
|
export function createTippy(target, opts = {}) {
|
||||||
// the callback functions should be destructured from opts,
|
// the callback functions should be destructured from opts,
|
||||||
// because we should use our own wrapper functions to handle them, do not let the user override them
|
// because we should use our own wrapper functions to handle them, do not let the user override them
|
||||||
const {onHide, onShow, onDestroy, role, theme, ...other} = opts;
|
const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
|
||||||
|
|
||||||
const instance = tippy(target, {
|
const instance = tippy(target, {
|
||||||
appendTo: document.body,
|
appendTo: document.body,
|
||||||
@ -35,9 +36,9 @@ export function createTippy(target, opts = {}) {
|
|||||||
visibleInstances.add(instance);
|
visibleInstances.add(instance);
|
||||||
return onShow?.(instance);
|
return onShow?.(instance);
|
||||||
},
|
},
|
||||||
arrow: `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`,
|
arrow: arrow || (theme === 'bare' ? false : arrowSvg),
|
||||||
role: role || 'menu', // HTML role attribute
|
role: role || 'menu', // HTML role attribute
|
||||||
theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu" or "box-with-header"
|
theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu", "box-with-header" or "bare"
|
||||||
plugins: [followCursor],
|
plugins: [followCursor],
|
||||||
...other,
|
...other,
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user