mirror of https://github.com/go-gitea/gitea
Merge branch 'main' into xfo
This commit is contained in:
commit
608b6e01c7
296
cmd/dump.go
296
cmd/dump.go
|
@ -6,14 +6,13 @@ package cmd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/dump"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
@ -25,89 +24,17 @@ import (
|
|||
"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.
|
||||
var CmdDump = &cli.Command{
|
||||
Name: "dump",
|
||||
Usage: "Dump Gitea files and database",
|
||||
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`,
|
||||
Action: runDump,
|
||||
Name: "dump",
|
||||
Usage: "Dump Gitea files and database",
|
||||
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`,
|
||||
Action: runDump,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "file",
|
||||
Aliases: []string{"f"},
|
||||
Value: fmt.Sprintf("gitea-dump-%d.zip", time.Now().Unix()),
|
||||
Usage: "Name of the dump file which will be created. Supply '-' for stdout. See type for available types.",
|
||||
Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
|
@ -160,64 +87,52 @@ It can be used for backup and capture Gitea server image to send to maintainer`,
|
|||
Name: "skip-index",
|
||||
Usage: "Skip bleve index data",
|
||||
},
|
||||
&cli.GenericFlag{
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Value: outputTypeEnum,
|
||||
Usage: fmt.Sprintf("Dump output format: %s", outputTypeEnum.Join()),
|
||||
Usage: fmt.Sprintf(`Dump output format, default to "zip", supported types: %s`, strings.Join(dump.SupportedOutputTypes, ", ")),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func fatal(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||
log.Fatal(format, args...)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
// make sure we are logging to the console no matter what the configuration tells us do to
|
||||
// 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
|
||||
|
||||
quite := ctx.Bool("quiet")
|
||||
verbose := ctx.Bool("verbose")
|
||||
if verbose && ctx.Bool("quiet") {
|
||||
return fmt.Errorf("--quiet and --verbose cannot both be set")
|
||||
if verbose && quite {
|
||||
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()
|
||||
defer cancel()
|
||||
|
||||
|
@ -226,44 +141,32 @@ func runDump(ctx *cli.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := storage.Init(); err != nil {
|
||||
if err = storage.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if file == nil {
|
||||
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)
|
||||
}
|
||||
archiverGeneric, err := archiver.ByExtension("." + outType)
|
||||
if err != nil {
|
||||
fatal("Unable to get archiver for extension: %v", err)
|
||||
}
|
||||
|
||||
w, _ := iface.(archiver.Writer)
|
||||
if err := w.Create(file); err != nil {
|
||||
archiverWriter := archiverGeneric.(archiver.Writer)
|
||||
if err := archiverWriter.Create(outFile); err != nil {
|
||||
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") {
|
||||
log.Info("Skip dumping local repositories")
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -276,8 +179,7 @@ func runDump(ctx *cli.Context) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addReader(w, object, info, path.Join("data", "lfs", objPath), verbose)
|
||||
return dumper.AddReader(object, info, path.Join("data", "lfs", objPath))
|
||||
}); err != nil {
|
||||
fatal("Failed to dump LFS objects: %v", err)
|
||||
}
|
||||
|
@ -310,15 +212,13 @@ func runDump(ctx *cli.Context) error {
|
|||
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)
|
||||
}
|
||||
|
||||
if len(setting.CustomConf) > 0 {
|
||||
log.Info("Adding custom configuration file from %s", setting.CustomConf)
|
||||
if err := addFile(w, "app.ini", setting.CustomConf, verbose); err != nil {
|
||||
fatal("Failed to include specified app.ini: %v", err)
|
||||
}
|
||||
log.Info("Adding custom configuration file from %s", setting.CustomConf)
|
||||
if err = dumper.AddFile("app.ini", setting.CustomConf); err != nil {
|
||||
fatal("Failed to include specified app.ini: %v", err)
|
||||
}
|
||||
|
||||
if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") {
|
||||
|
@ -326,8 +226,8 @@ func runDump(ctx *cli.Context) error {
|
|||
} else {
|
||||
customDir, err := os.Stat(setting.CustomPath)
|
||||
if err == nil && customDir.IsDir() {
|
||||
if is, _ := isSubdir(setting.AppDataPath, setting.CustomPath); !is {
|
||||
if err := addRecursiveExclude(w, "custom", setting.CustomPath, []string{absFileName}, verbose); err != nil {
|
||||
if is, _ := dump.IsSubdir(setting.AppDataPath, setting.CustomPath); !is {
|
||||
if err := dumper.AddRecursiveExclude("custom", setting.CustomPath, nil); err != nil {
|
||||
fatal("Failed to include custom: %v", err)
|
||||
}
|
||||
} else {
|
||||
|
@ -364,8 +264,7 @@ func runDump(ctx *cli.Context) error {
|
|||
excludes = append(excludes, setting.Attachment.Storage.Path)
|
||||
excludes = append(excludes, setting.Packages.Storage.Path)
|
||||
excludes = append(excludes, setting.Log.RootPath)
|
||||
excludes = append(excludes, absFileName)
|
||||
if err := addRecursiveExclude(w, "data", setting.AppDataPath, excludes, verbose); err != nil {
|
||||
if err := dumper.AddRecursiveExclude("data", setting.AppDataPath, excludes); err != nil {
|
||||
fatal("Failed to include data directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
@ -377,8 +276,7 @@ func runDump(ctx *cli.Context) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addReader(w, object, info, path.Join("data", "attachments", objPath), verbose)
|
||||
return dumper.AddReader(object, info, path.Join("data", "attachments", objPath))
|
||||
}); err != nil {
|
||||
fatal("Failed to dump attachments: %v", err)
|
||||
}
|
||||
|
@ -392,8 +290,7 @@ func runDump(ctx *cli.Context) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addReader(w, object, info, path.Join("data", "packages", objPath), verbose)
|
||||
return dumper.AddReader(object, info, path.Join("data", "packages", objPath))
|
||||
}); err != nil {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fileName != "-" {
|
||||
if err = w.Close(); err != nil {
|
||||
_ = util.Remove(fileName)
|
||||
fatal("Failed to save %s: %v", fileName, err)
|
||||
if outFileName == "-" {
|
||||
log.Info("Finish dumping to stdout")
|
||||
} else {
|
||||
if err = archiverWriter.Close(); err != nil {
|
||||
_ = os.Remove(outFileName)
|
||||
fatal("Failed to save %q: %v", outFileName, err)
|
||||
}
|
||||
|
||||
if err := os.Chmod(fileName, 0o600); err != nil {
|
||||
if err = os.Chmod(outFileName, 0o600); err != nil {
|
||||
log.Info("Can't change file access permissions mask to 0600: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Info("Finish dumping in file %s", outFileName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -545,7 +545,7 @@ In this option, the idea is that the host SSH uses an `AuthorizedKeysCommand` in
|
|||
```bash
|
||||
cat <<"EOF" | sudo tee /home/git/docker-shell
|
||||
#!/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
|
||||
sudo chmod +x /home/git/docker-shell
|
||||
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
|
||||
Match User 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.)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
var initLoggerDisabled bool
|
||||
|
||||
// initAllLoggers creates all the log services
|
||||
func initAllLoggers() {
|
||||
if initLoggerDisabled {
|
||||
return
|
||||
}
|
||||
initManagedLoggers(log.GetManager(), CfgProvider)
|
||||
|
||||
golog.SetFlags(0)
|
||||
|
@ -194,6 +199,10 @@ func initAllLoggers() {
|
|||
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
|
||||
}
|
||||
|
||||
func DisableLoggerInit() {
|
||||
initLoggerDisabled = true
|
||||
}
|
||||
|
||||
func initManagedLoggers(manager *log.LoggerManager, cfg ConfigProvider) {
|
||||
loadLogGlobalFrom(cfg)
|
||||
prepareLoggerConfig(cfg)
|
||||
|
|
|
@ -21,8 +21,9 @@ var (
|
|||
)
|
||||
|
||||
// MockSet sets the time to a mocked time.Time
|
||||
func MockSet(now time.Time) {
|
||||
func MockSet(now time.Time) func() {
|
||||
mockNow = now
|
||||
return MockUnset
|
||||
}
|
||||
|
||||
// MockUnset will unset the mocked time.Time
|
||||
|
|
|
@ -213,6 +213,14 @@ func ToPointer[T any](val T) *T {
|
|||
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"
|
||||
func IfZero[T comparable](v, def T) T {
|
||||
var zero T
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
"@github/relative-time-element": "4.4.0",
|
||||
"@github/text-expander-element": "2.6.1",
|
||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||
"@melloware/coloris": "0.23.0",
|
||||
"@primer/octicons": "19.9.0",
|
||||
"add-asset-webpack-plugin": "2.0.1",
|
||||
"ansi_up": "6.0.2",
|
||||
|
@ -54,6 +53,7 @@
|
|||
"toastify-js": "1.12.0",
|
||||
"tributejs": "5.1.3",
|
||||
"uint8-to-base64": "0.2.0",
|
||||
"vanilla-colorful": "0.7.2",
|
||||
"vue": "3.4.21",
|
||||
"vue-bar-graph": "2.0.0",
|
||||
"vue-chartjs": "5.3.0",
|
||||
|
@ -1290,11 +1290,6 @@
|
|||
"@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": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
@ -11853,6 +11848,11 @@
|
|||
"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": {
|
||||
"version": "5.2.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz",
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
"@github/relative-time-element": "4.4.0",
|
||||
"@github/text-expander-element": "2.6.1",
|
||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||
"@melloware/coloris": "0.23.0",
|
||||
"@primer/octicons": "19.9.0",
|
||||
"add-asset-webpack-plugin": "2.0.1",
|
||||
"ansi_up": "6.0.2",
|
||||
|
@ -53,6 +52,7 @@
|
|||
"toastify-js": "1.12.0",
|
||||
"tributejs": "5.1.3",
|
||||
"uint8-to-base64": "0.2.0",
|
||||
"vanilla-colorful": "0.7.2",
|
||||
"vue": "3.4.21",
|
||||
"vue-bar-graph": "2.0.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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.js-color-picker-input input {
|
||||
|
@ -13,152 +9,39 @@
|
|||
padding-left: 32px !important;
|
||||
}
|
||||
|
||||
.clr-picker {
|
||||
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 {
|
||||
.js-color-picker-input .preview-square {
|
||||
position: absolute;
|
||||
aspect-ratio: 1;
|
||||
height: 16px;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
pointer-events: none;
|
||||
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-position: 0 0, 4px 4px;
|
||||
background-size: 8px 8px;
|
||||
}
|
||||
|
||||
.clr-field button::after {
|
||||
.js-color-picker-input .preview-square::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: inherit;
|
||||
background-color: currentcolor;
|
||||
}
|
||||
|
||||
.clr-marker:focus {
|
||||
outline: none;
|
||||
hex-color-picker {
|
||||
width: 180px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.clr-keyboard-nav .clr-marker:focus,
|
||||
.clr-keyboard-nav .clr-hue input:focus + div,
|
||||
.clr-keyboard-nav .clr-alpha input:focus + div {
|
||||
outline: none;
|
||||
box-shadow: 0 0 2px 2px var(--color-white);
|
||||
hex-color-picker::part(hue-pointer),
|
||||
hex-color-picker::part(saturation-pointer) {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.clr-picker .clr-preview,
|
||||
.clr-picker .clr-clear,
|
||||
.clr-picker .clr-swatches,
|
||||
.clr-picker .clr-format,
|
||||
.clr-picker .clr-alpha,
|
||||
.clr-picker .clr-color {
|
||||
display: none;
|
||||
hex-color-picker::part(hue) {
|
||||
flex-basis: 16px;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
.markup .code-preview-container table {
|
||||
width: 100%;
|
||||
max-height: 100px;
|
||||
max-height: 240px; /* 12 lines at 20px per line */
|
||||
overflow-y: auto;
|
||||
margin: 0; /* override ".markup table {margin}" */
|
||||
}
|
||||
|
|
|
@ -29,6 +29,17 @@
|
|||
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 */
|
||||
|
||||
.tippy-box[data-theme="tooltip"] {
|
||||
|
|
|
@ -1,31 +1,66 @@
|
|||
export async function initColorPickers(selector = '.js-color-picker-input input', opts = {}) {
|
||||
const inputEls = document.querySelectorAll(selector);
|
||||
if (!inputEls.length) return;
|
||||
import {createTippy} from '../modules/tippy.js';
|
||||
|
||||
const [{coloris, init}] = await Promise.all([
|
||||
import(/* webpackChunkName: "colorpicker" */'@melloware/coloris'),
|
||||
export async function initColorPickers() {
|
||||
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'),
|
||||
]);
|
||||
|
||||
init();
|
||||
coloris({
|
||||
el: selector,
|
||||
alpha: false,
|
||||
focusInput: true,
|
||||
selectInput: false,
|
||||
...opts,
|
||||
});
|
||||
|
||||
for (const inputEl of inputEls) {
|
||||
const parent = inputEl.closest('.js-color-picker-input');
|
||||
// prevent tabbing on the color preview `button` inside the input
|
||||
parent.querySelector('button').tabIndex = -1;
|
||||
// init precolors
|
||||
for (const el of parent.querySelectorAll('.precolors .color')) {
|
||||
el.addEventListener('click', (e) => {
|
||||
inputEl.value = e.target.getAttribute('data-color-hex');
|
||||
inputEl.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
});
|
||||
}
|
||||
for (const el of els) {
|
||||
initPicker(el);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSquare(el, newValue) {
|
||||
el.style.color = /#[0-9a-f]{6}/i.test(newValue) ? newValue : 'transparent';
|
||||
}
|
||||
|
||||
function updatePicker(el, newValue) {
|
||||
el.setAttribute('color', newValue);
|
||||
}
|
||||
|
||||
function initPicker(el) {
|
||||
const input = el.querySelector('input');
|
||||
|
||||
const square = document.createElement('div');
|
||||
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';
|
||||
|
||||
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 = {}) {
|
||||
// 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
|
||||
const {onHide, onShow, onDestroy, role, theme, ...other} = opts;
|
||||
const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
|
||||
|
||||
const instance = tippy(target, {
|
||||
appendTo: document.body,
|
||||
|
@ -35,9 +36,9 @@ export function createTippy(target, opts = {}) {
|
|||
visibleInstances.add(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
|
||||
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],
|
||||
...other,
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue