mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 00:50:31 +00:00
memory provider: load users from a dump file
The `memory` provider can load users from a dump obtained using the `dumpdata` REST API. This dump file can be configured using the dataprovider `name` configuration key. It will be loaded at startup and can be reloaded on demand using a `SIGHUP` on Unix based systems and a `paramchange` request to the running service on Windows. Fixes #66
This commit is contained in:
parent
31a433cda2
commit
bcaf283c35
22 changed files with 284 additions and 60 deletions
|
@ -157,7 +157,7 @@ The `sftpgo` configuration file contains the following sections:
|
|||
- `keyboard_interactive_auth_program`, string. Absolute path to an external program to use for keyboard interactive authentication. See the "Keyboard Interactive Authentication" paragraph for more details.
|
||||
- **"data_provider"**, the configuration for the data provider
|
||||
- `driver`, string. Supported drivers are `sqlite`, `mysql`, `postgresql`, `bolt`, `memory`
|
||||
- `name`, string. Database name. For driver `sqlite` this can be the database name relative to the config dir or the absolute path to the SQLite database.
|
||||
- `name`, string. Database name. For driver `sqlite` this can be the database name relative to the config dir or the absolute path to the SQLite database. For driver `memory` this is the (optional) path relative to the config dir or the absolute path to the users dump to load.
|
||||
- `host`, string. Database host. Leave empty for drivers `sqlite`, `bolt` and `memory`
|
||||
- `port`, integer. Database port. Leave empty for drivers `sqlite`, `bolt` and `memory`
|
||||
- `username`, string. Database user. Leave empty for drivers `sqlite`, `bolt` and `memory`
|
||||
|
@ -277,7 +277,9 @@ Before starting `sftpgo serve` please ensure that the configured dataprovider is
|
|||
SQL based data providers (SQLite, MySQL, PostgreSQL) requires the creation of a database containing the required tables. Memory and bolt data providers does not require an initialization.
|
||||
|
||||
SQL scripts to create the required database structure can be found inside the source tree [sql](./sql "sql") directory. The SQL scripts filename is, by convention, the date as `YYYYMMDD` and the suffix `.sql`. You need to apply all the SQL scripts for your database ordered by name, for example `20190828.sql` must be applied before `20191112.sql` and so on.
|
||||
Example for `SQLite`: `find sql/sqlite/ -type f -iname '*.sql' -print | sort -n |xargs cat | sqlite3 sftpgo.db`
|
||||
Example for SQLite: `find sql/sqlite/ -type f -iname '*.sql' -print | sort -n |xargs cat | sqlite3 sftpgo.db`.
|
||||
|
||||
The `memory` provider can load users from a dump obtained using the `dumpdata` REST API. This dump file can be configured using the dataprovider `name` configuration key. It will be loaded at startup and can be reloaded on demand using a `SIGHUP` on Unix based systems and a `paramchange` request to the running service on Windows.
|
||||
|
||||
### Starting SFTGo in server mode
|
||||
|
||||
|
@ -291,13 +293,14 @@ On Windows you can register `SFTPGo` as Windows Service, take a look at the CLI
|
|||
|
||||
```
|
||||
sftpgo.exe service --help
|
||||
Install, Uninstall, Start, Stop and retrieve status for SFTPGo Windows Service
|
||||
Install, Uninstall, Start, Stop, Reload and retrieve status for SFTPGo Windows Service
|
||||
|
||||
Usage:
|
||||
sftpgo service [command]
|
||||
|
||||
Available Commands:
|
||||
install Install SFTPGo as Windows Service
|
||||
reload Reload the SFTPGo Windows Service sending a `paramchange` request
|
||||
start Start SFTPGo Windows Service
|
||||
status Retrieve the status for the SFTPGo Windows Service
|
||||
stop Stop SFTPGo Windows Service
|
||||
|
|
32
cmd/reload_windows.go
Normal file
32
cmd/reload_windows.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/drakkan/sftpgo/service"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
reloadCmd = &cobra.Command{
|
||||
Use: "reload",
|
||||
Short: "Reload the SFTPGo Windows Service sending a \"paramchange\" request",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
s := service.WindowsService{
|
||||
Service: service.Service{
|
||||
Shutdown: make(chan bool),
|
||||
},
|
||||
}
|
||||
err := s.Reload()
|
||||
if err != nil {
|
||||
fmt.Printf("Error reloading service: %v\r\n", err)
|
||||
} else {
|
||||
fmt.Printf("Service reloaded!\r\n")
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
serviceCmd.AddCommand(reloadCmd)
|
||||
}
|
|
@ -7,7 +7,7 @@ import (
|
|||
var (
|
||||
serviceCmd = &cobra.Command{
|
||||
Use: "service",
|
||||
Short: "Install, Uninstall, Start, Stop and retrieve status for SFTPGo Windows Service",
|
||||
Short: "Install, Uninstall, Start, Stop, Reload and retrieve status for SFTPGo Windows Service",
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -392,6 +392,10 @@ func (p BoltProvider) close() error {
|
|||
return p.dbHandle.Close()
|
||||
}
|
||||
|
||||
func (p BoltProvider) reloadConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// itob returns an 8-byte big endian representation of v.
|
||||
func itob(v int64) []byte {
|
||||
b := make([]byte, 8)
|
||||
|
|
|
@ -187,6 +187,11 @@ type Config struct {
|
|||
CredentialsPath string `json:"credentials_path" mapstructure:"credentials_path"`
|
||||
}
|
||||
|
||||
// BackupData defines the structure for the backup/restore files
|
||||
type BackupData struct {
|
||||
Users []User `json:"users"`
|
||||
}
|
||||
|
||||
type keyboardAuthProgramResponse struct {
|
||||
Instruction string `json:"instruction"`
|
||||
Questions []string `json:"questions"`
|
||||
|
@ -251,6 +256,7 @@ type Provider interface {
|
|||
updateLastLogin(username string) error
|
||||
checkAvailability() error
|
||||
close() error
|
||||
reloadConfig() error
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -287,7 +293,7 @@ func Initialize(cnf Config, basePath string) error {
|
|||
} else if config.Driver == BoltDataProviderName {
|
||||
err = initializeBoltProvider(basePath)
|
||||
} else if config.Driver == MemoryDataProviderName {
|
||||
err = initializeMemoryProvider()
|
||||
err = initializeMemoryProvider(basePath)
|
||||
} else {
|
||||
err = fmt.Errorf("unsupported data provider: %v", config.Driver)
|
||||
}
|
||||
|
@ -417,6 +423,13 @@ func DumpUsers(p Provider) ([]User, error) {
|
|||
return p.dumpUsers()
|
||||
}
|
||||
|
||||
// ReloadConfig reloads provider configuration.
|
||||
// Currently only implemented for memory provider, allows to reload the users
|
||||
// from the configured file, if defined
|
||||
func ReloadConfig() error {
|
||||
return provider.reloadConfig()
|
||||
}
|
||||
|
||||
// GetUsers returns an array of users respecting limit and offset and filtered by username exact match if not empty
|
||||
func GetUsers(p Provider, limit int, offset int, order string, username string) ([]User, error) {
|
||||
return p.getUsers(limit, offset, order, username)
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
package dataprovider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -23,7 +27,9 @@ type memoryProviderHandle struct {
|
|||
usersIdx map[int64]string
|
||||
// map for users, username is the key
|
||||
users map[string]User
|
||||
lock *sync.Mutex
|
||||
// configuration file to use for loading users
|
||||
configFile string
|
||||
lock *sync.Mutex
|
||||
}
|
||||
|
||||
// MemoryProvider auth provider for a memory store
|
||||
|
@ -31,17 +37,25 @@ type MemoryProvider struct {
|
|||
dbHandle *memoryProviderHandle
|
||||
}
|
||||
|
||||
func initializeMemoryProvider() error {
|
||||
func initializeMemoryProvider(basePath string) error {
|
||||
configFile := ""
|
||||
if len(config.Name) > 0 {
|
||||
configFile = config.Name
|
||||
if !filepath.IsAbs(configFile) {
|
||||
configFile = filepath.Join(basePath, configFile)
|
||||
}
|
||||
}
|
||||
provider = MemoryProvider{
|
||||
dbHandle: &memoryProviderHandle{
|
||||
isClosed: false,
|
||||
usernames: []string{},
|
||||
usersIdx: make(map[int64]string),
|
||||
users: make(map[string]User),
|
||||
lock: new(sync.Mutex),
|
||||
isClosed: false,
|
||||
usernames: []string{},
|
||||
usersIdx: make(map[int64]string),
|
||||
users: make(map[string]User),
|
||||
configFile: configFile,
|
||||
lock: new(sync.Mutex),
|
||||
},
|
||||
}
|
||||
return nil
|
||||
return provider.reloadConfig()
|
||||
}
|
||||
|
||||
func (p MemoryProvider) checkAvailability() error {
|
||||
|
@ -308,3 +322,71 @@ func (p MemoryProvider) getNextID() int64 {
|
|||
}
|
||||
return nextID
|
||||
}
|
||||
|
||||
func (p MemoryProvider) clearUsers() {
|
||||
p.dbHandle.lock.Lock()
|
||||
defer p.dbHandle.lock.Unlock()
|
||||
p.dbHandle.usernames = []string{}
|
||||
p.dbHandle.usersIdx = make(map[int64]string)
|
||||
p.dbHandle.users = make(map[string]User)
|
||||
}
|
||||
|
||||
func (p MemoryProvider) reloadConfig() error {
|
||||
if len(p.dbHandle.configFile) == 0 {
|
||||
providerLog(logger.LevelDebug, "no users configuration file defined")
|
||||
return nil
|
||||
}
|
||||
providerLog(logger.LevelDebug, "loading users from file: %#v", p.dbHandle.configFile)
|
||||
fi, err := os.Stat(p.dbHandle.configFile)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error loading users: %v", err)
|
||||
return err
|
||||
}
|
||||
if fi.Size() == 0 {
|
||||
err = errors.New("users configuration file is invalid, its size must be > 0")
|
||||
providerLog(logger.LevelWarn, "error loading users: %v", err)
|
||||
return err
|
||||
}
|
||||
if fi.Size() > 10485760 {
|
||||
err = errors.New("users configuration file is invalid, its size must be <= 10485760 bytes")
|
||||
providerLog(logger.LevelWarn, "error loading users: %v", err)
|
||||
return err
|
||||
}
|
||||
content, err := ioutil.ReadFile(p.dbHandle.configFile)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error loading users: %v", err)
|
||||
return err
|
||||
}
|
||||
var dump BackupData
|
||||
err = json.Unmarshal(content, &dump)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error loading users: %v", err)
|
||||
return err
|
||||
}
|
||||
p.clearUsers()
|
||||
for _, user := range dump.Users {
|
||||
u, err := p.userExists(user.Username)
|
||||
if err == nil {
|
||||
user.ID = u.ID
|
||||
user.LastLogin = u.LastLogin
|
||||
user.UsedQuotaSize = u.UsedQuotaSize
|
||||
user.UsedQuotaFiles = u.UsedQuotaFiles
|
||||
err = p.updateUser(user)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error updating user %#v: %v", user.Username, err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
user.LastLogin = 0
|
||||
user.UsedQuotaSize = 0
|
||||
user.UsedQuotaFiles = 0
|
||||
err = p.addUser(user)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelWarn, "error adding user %#v: %v", user.Username, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
providerLog(logger.LevelDebug, "users loaded from file: %#v", p.dbHandle.configFile)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -99,3 +99,7 @@ func (p MySQLProvider) getUsers(limit int, offset int, order string, username st
|
|||
func (p MySQLProvider) close() error {
|
||||
return p.dbHandle.Close()
|
||||
}
|
||||
|
||||
func (p MySQLProvider) reloadConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -98,3 +98,7 @@ func (p PGSQLProvider) getUsers(limit int, offset int, order string, username st
|
|||
func (p PGSQLProvider) close() error {
|
||||
return p.dbHandle.Close()
|
||||
}
|
||||
|
||||
func (p PGSQLProvider) reloadConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -105,3 +105,7 @@ func (p SQLiteProvider) getUsers(limit int, offset int, order string, username s
|
|||
func (p SQLiteProvider) close() error {
|
||||
return p.dbHandle.Close()
|
||||
}
|
||||
|
||||
func (p SQLiteProvider) reloadConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -17,10 +17,13 @@ import (
|
|||
)
|
||||
|
||||
func dumpData(w http.ResponseWriter, r *http.Request) {
|
||||
var outputFile string
|
||||
var outputFile, indent string
|
||||
if _, ok := r.URL.Query()["output_file"]; ok {
|
||||
outputFile = strings.TrimSpace(r.URL.Query().Get("output_file"))
|
||||
}
|
||||
if _, ok := r.URL.Query()["indent"]; ok {
|
||||
indent = strings.TrimSpace(r.URL.Query().Get("indent"))
|
||||
}
|
||||
if len(outputFile) == 0 {
|
||||
sendAPIResponse(w, r, errors.New("Invalid or missing output_file"), "", http.StatusBadRequest)
|
||||
return
|
||||
|
@ -42,12 +45,19 @@ func dumpData(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
dump, err := json.Marshal(BackupData{
|
||||
Users: users,
|
||||
})
|
||||
var dump []byte
|
||||
if indent == "1" {
|
||||
dump, err = json.MarshalIndent(dataprovider.BackupData{
|
||||
Users: users,
|
||||
}, "", " ")
|
||||
} else {
|
||||
dump, err = json.Marshal(dataprovider.BackupData{
|
||||
Users: users,
|
||||
})
|
||||
}
|
||||
if err == nil {
|
||||
os.MkdirAll(filepath.Dir(outputFile), 0777)
|
||||
err = ioutil.WriteFile(outputFile, dump, 0666)
|
||||
os.MkdirAll(filepath.Dir(outputFile), 0700)
|
||||
err = ioutil.WriteFile(outputFile, dump, 0600)
|
||||
}
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "", "dumping data error: %v, output file: %#v", err, outputFile)
|
||||
|
@ -74,8 +84,8 @@ func loadData(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
if fi.Size() > maxRestoreSize {
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to restore input file: %#v size too big: %v/%v", inputFile, fi.Size(),
|
||||
maxRestoreSize), http.StatusBadRequest)
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to restore input file: %#v size too big: %v/%v bytes",
|
||||
inputFile, fi.Size(), maxRestoreSize), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -84,7 +94,7 @@ func loadData(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
var dump BackupData
|
||||
var dump dataprovider.BackupData
|
||||
err = json.Unmarshal(content, &dump)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to parse input file: %#v", inputFile), http.StatusBadRequest)
|
||||
|
@ -95,7 +105,7 @@ func loadData(w http.ResponseWriter, r *http.Request) {
|
|||
u, err := dataprovider.UserExists(dataProvider, user.Username)
|
||||
if err == nil {
|
||||
if mode == 1 {
|
||||
logger.Debug(logSender, "", "loaddata mode = 1 existing user: %#v not updated", u.Username)
|
||||
logger.Debug(logSender, "", "loaddata mode 1, existing user %#v not updated", u.Username)
|
||||
continue
|
||||
}
|
||||
user.ID = u.ID
|
||||
|
|
|
@ -309,7 +309,7 @@ func GetProviderStatus(expectedStatusCode int) (map[string]interface{}, []byte,
|
|||
|
||||
// Dumpdata requests a backup to outputFile.
|
||||
// outputFile is relative to the configured backups_path
|
||||
func Dumpdata(outputFile string, expectedStatusCode int) (map[string]interface{}, []byte, error) {
|
||||
func Dumpdata(outputFile, indent string, expectedStatusCode int) (map[string]interface{}, []byte, error) {
|
||||
var response map[string]interface{}
|
||||
var body []byte
|
||||
url, err := url.Parse(buildURLRelativeToBase(dumpDataPath))
|
||||
|
@ -318,6 +318,9 @@ func Dumpdata(outputFile string, expectedStatusCode int) (map[string]interface{}
|
|||
}
|
||||
q := url.Query()
|
||||
q.Add("output_file", outputFile)
|
||||
if len(indent) > 0 {
|
||||
q.Add("indent", indent)
|
||||
}
|
||||
url.RawQuery = q.Encode()
|
||||
resp, err := getHTTPClient().Get(url.String())
|
||||
if err != nil {
|
||||
|
|
|
@ -56,11 +56,6 @@ type Conf struct {
|
|||
BackupsPath string `json:"backups_path" mapstructure:"backups_path"`
|
||||
}
|
||||
|
||||
// BackupData defines the structure for the backup/restore files
|
||||
type BackupData struct {
|
||||
Users []dataprovider.User `json:"users"`
|
||||
}
|
||||
|
||||
type apiResponse struct {
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
|
|
|
@ -716,13 +716,13 @@ func TestProviderErrors(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Errorf("get provider status with provider closed must fail: %v", err)
|
||||
}
|
||||
_, _, err = httpd.Dumpdata("backup.json", http.StatusInternalServerError)
|
||||
_, _, err = httpd.Dumpdata("backup.json", "", http.StatusInternalServerError)
|
||||
if err != nil {
|
||||
t.Errorf("get provider status with provider closed must fail: %v", err)
|
||||
}
|
||||
user := getTestUser()
|
||||
user.ID = 1
|
||||
backupData := httpd.BackupData{}
|
||||
backupData := dataprovider.BackupData{}
|
||||
backupData.Users = append(backupData.Users, user)
|
||||
backupContent, _ := json.Marshal(backupData)
|
||||
backupFilePath := filepath.Join(backupsPath, "backup.json")
|
||||
|
@ -755,26 +755,30 @@ func TestDumpdata(t *testing.T) {
|
|||
}
|
||||
httpd.SetDataProvider(dataprovider.GetProvider())
|
||||
sftpd.SetDataProvider(dataprovider.GetProvider())
|
||||
_, _, err = httpd.Dumpdata("", http.StatusBadRequest)
|
||||
_, _, err = httpd.Dumpdata("", "", http.StatusBadRequest)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
_, _, err = httpd.Dumpdata(filepath.Join(backupsPath, "backup.json"), http.StatusBadRequest)
|
||||
_, _, err = httpd.Dumpdata(filepath.Join(backupsPath, "backup.json"), "", http.StatusBadRequest)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
_, _, err = httpd.Dumpdata("../backup.json", http.StatusBadRequest)
|
||||
_, _, err = httpd.Dumpdata("../backup.json", "", http.StatusBadRequest)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
_, _, err = httpd.Dumpdata("backup.json", http.StatusOK)
|
||||
_, _, err = httpd.Dumpdata("backup.json", "0", http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
_, _, err = httpd.Dumpdata("backup.json", "1", http.StatusOK)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
os.Remove(filepath.Join(backupsPath, "backup.json"))
|
||||
if runtime.GOOS != "windows" {
|
||||
os.Chmod(backupsPath, 0001)
|
||||
_, _, err = httpd.Dumpdata("bck.json", http.StatusInternalServerError)
|
||||
_, _, err = httpd.Dumpdata("bck.json", "", http.StatusInternalServerError)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
@ -795,7 +799,7 @@ func TestLoaddata(t *testing.T) {
|
|||
user := getTestUser()
|
||||
user.ID = 1
|
||||
user.Username = "test_user_restore"
|
||||
backupData := httpd.BackupData{}
|
||||
backupData := dataprovider.BackupData{}
|
||||
backupData.Users = append(backupData.Users, user)
|
||||
backupContent, _ := json.Marshal(backupData)
|
||||
backupFilePath := filepath.Join(backupsPath, "backup.json")
|
||||
|
@ -865,7 +869,7 @@ func TestLoaddataMode(t *testing.T) {
|
|||
user := getTestUser()
|
||||
user.ID = 1
|
||||
user.Username = "test_user_restore"
|
||||
backupData := httpd.BackupData{}
|
||||
backupData := dataprovider.BackupData{}
|
||||
backupData.Users = append(backupData.Users, user)
|
||||
backupContent, _ := json.Marshal(backupData)
|
||||
backupFilePath := filepath.Join(backupsPath, "backup.json")
|
||||
|
|
|
@ -336,7 +336,7 @@ func TestApiCallsWithBadURL(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Error("request with invalid URL must fail")
|
||||
}
|
||||
_, _, err = Dumpdata("backup.json", http.StatusBadRequest)
|
||||
_, _, err = Dumpdata("backup.json", "", http.StatusBadRequest)
|
||||
if err == nil {
|
||||
t.Error("request with invalid URL must fail")
|
||||
}
|
||||
|
@ -395,7 +395,7 @@ func TestApiCallToNotListeningServer(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Errorf("request to an inactive URL must fail")
|
||||
}
|
||||
_, _, err = Dumpdata("backup.json", http.StatusOK)
|
||||
_, _, err = Dumpdata("backup.json", "0", http.StatusOK)
|
||||
if err == nil {
|
||||
t.Errorf("request to an inactive URL must fail")
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ openapi: 3.0.1
|
|||
info:
|
||||
title: SFTPGo
|
||||
description: 'SFTPGo REST API'
|
||||
version: 1.6.1
|
||||
version: 1.6.2
|
||||
|
||||
servers:
|
||||
- url: /api/v1
|
||||
|
@ -543,6 +543,17 @@ paths:
|
|||
type: string
|
||||
required: true
|
||||
description: Path for the file to write the JSON serialized data to. This path is relative to the configured "backups_path". If this file already exists it will be overwritten
|
||||
- in: query
|
||||
name: indent
|
||||
schema:
|
||||
type: integer
|
||||
enum:
|
||||
- 0
|
||||
- 1
|
||||
description: >
|
||||
indent:
|
||||
* `0` no indentation. This is the default
|
||||
* `1` format the output JSON
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
|
|
|
@ -368,7 +368,7 @@ Output:
|
|||
Command:
|
||||
|
||||
```
|
||||
python sftpgo_api_cli.py dumpdata backup.json
|
||||
python sftpgo_api_cli.py dumpdata backup.json --indent 1
|
||||
```
|
||||
|
||||
Output:
|
||||
|
@ -386,7 +386,7 @@ Output:
|
|||
Command:
|
||||
|
||||
```
|
||||
python sftpgo_api_cli.py loaddata /app/data/backups/backup.json --scan-quota 2
|
||||
python sftpgo_api_cli.py loaddata /app/data/backups/backup.json --scan-quota 2 --mode 0
|
||||
```
|
||||
|
||||
Output:
|
||||
|
|
|
@ -209,9 +209,9 @@ class SFTPGoApiRequests:
|
|||
r = requests.get(self.providerStatusPath, auth=self.auth, verify=self.verify)
|
||||
self.printResponse(r)
|
||||
|
||||
def dumpData(self, output_file):
|
||||
r = requests.get(self.dumpDataPath, params={'output_file':output_file}, auth=self.auth,
|
||||
verify=self.verify)
|
||||
def dumpData(self, output_file, indent):
|
||||
r = requests.get(self.dumpDataPath, params={'output_file':output_file, 'indent':indent},
|
||||
auth=self.auth, verify=self.verify)
|
||||
self.printResponse(r)
|
||||
|
||||
def loadData(self, input_file, scan_quota, mode):
|
||||
|
@ -514,6 +514,8 @@ if __name__ == '__main__':
|
|||
|
||||
parserDumpData = subparsers.add_parser('dumpdata', help='Backup SFTPGo data serializing them as JSON')
|
||||
parserDumpData.add_argument('output_file', type=str)
|
||||
parserDumpData.add_argument('-I', '--indent', type=int, choices=[0, 1], default=0,
|
||||
help='0 means no indentation. 1 means format the output JSON. Default: %(default)s')
|
||||
|
||||
parserLoadData = subparsers.add_parser('loaddata', help='Restore SFTPGo data from a JSON backup')
|
||||
parserLoadData.add_argument('input_file', type=str)
|
||||
|
@ -584,7 +586,7 @@ if __name__ == '__main__':
|
|||
elif args.command == 'get-provider-status':
|
||||
api.getProviderStatus()
|
||||
elif args.command == 'dumpdata':
|
||||
api.dumpData(args.output_file)
|
||||
api.dumpData(args.output_file, args.indent)
|
||||
elif args.command == 'loaddata':
|
||||
api.loadData(args.input_file, args.scan_quota, args.mode)
|
||||
elif args.command == 'convert-users':
|
||||
|
|
|
@ -114,6 +114,9 @@ func (s *Service) Start() error {
|
|||
logger.DebugToConsole("HTTP server not started, disabled in config file")
|
||||
}
|
||||
}
|
||||
if s.PortableMode != 1 {
|
||||
registerSigHup()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
|
||||
"golang.org/x/sys/windows/svc"
|
||||
|
@ -61,7 +62,7 @@ func (s Status) String() string {
|
|||
}
|
||||
|
||||
func (s *WindowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
|
||||
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
|
||||
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptParamChange
|
||||
changes <- svc.Status{State: svc.StartPending}
|
||||
if err := s.Service.Start(); err != nil {
|
||||
return true, 1
|
||||
|
@ -79,6 +80,9 @@ loop:
|
|||
changes <- svc.Status{State: svc.StopPending}
|
||||
s.Service.Stop()
|
||||
break loop
|
||||
case svc.ParamChange:
|
||||
logger.Debug(logSender, "", "Received reload request")
|
||||
dataprovider.ReloadConfig()
|
||||
default:
|
||||
continue loop
|
||||
}
|
||||
|
@ -127,6 +131,24 @@ func (s *WindowsService) Start() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *WindowsService) Reload() error {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.Disconnect()
|
||||
service, err := m.OpenService(serviceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not access service: %v", err)
|
||||
}
|
||||
defer service.Close()
|
||||
_, err = service.Control(svc.ParamChange)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not send control=%d: %v", svc.ParamChange, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WindowsService) Install(args ...string) error {
|
||||
exePath, err := s.getExePath()
|
||||
if err != nil {
|
||||
|
|
23
service/sighup_unix.go
Normal file
23
service/sighup_unix.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
// +build !windows
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
)
|
||||
|
||||
func registerSigHup() {
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGHUP)
|
||||
go func() {
|
||||
for range sig {
|
||||
logger.Debug(logSender, "", "Received reload request")
|
||||
dataprovider.ReloadConfig()
|
||||
}
|
||||
}()
|
||||
}
|
4
service/sighup_windows.go
Normal file
4
service/sighup_windows.go
Normal file
|
@ -0,0 +1,4 @@
|
|||
package service
|
||||
|
||||
func registerSigHup() {
|
||||
}
|
|
@ -3035,12 +3035,13 @@ func TestRelativePaths(t *testing.T) {
|
|||
user := getTestUser(true)
|
||||
var path, rel string
|
||||
filesystems := []vfs.Fs{vfs.NewOsFs("", user.GetHomeDir())}
|
||||
keyPrefix := strings.TrimPrefix(user.GetHomeDir(), "/") + "/"
|
||||
s3config := vfs.S3FsConfig{
|
||||
KeyPrefix: strings.TrimPrefix(user.GetHomeDir(), "/") + "/",
|
||||
KeyPrefix: keyPrefix,
|
||||
}
|
||||
s3fs, _ := vfs.NewS3Fs("", user.GetHomeDir(), s3config)
|
||||
gcsConfig := vfs.GCSFsConfig{
|
||||
KeyPrefix: strings.TrimPrefix(user.GetHomeDir(), "/") + "/",
|
||||
KeyPrefix: keyPrefix,
|
||||
}
|
||||
gcsfs, _ := vfs.NewGCSFs("", user.GetHomeDir(), gcsConfig)
|
||||
filesystems = append(filesystems, s3fs, gcsfs)
|
||||
|
@ -3048,52 +3049,52 @@ func TestRelativePaths(t *testing.T) {
|
|||
path = filepath.Join(user.HomeDir, "/")
|
||||
rel = fs.GetRelativePath(path)
|
||||
if rel != "/" {
|
||||
t.Errorf("Unexpected relative path: %v", rel)
|
||||
t.Errorf("Unexpected relative path: %v fs: %v", rel, fs.Name())
|
||||
}
|
||||
path = filepath.Join(user.HomeDir, "//")
|
||||
rel = fs.GetRelativePath(path)
|
||||
if rel != "/" {
|
||||
t.Errorf("Unexpected relative path: %v", rel)
|
||||
t.Errorf("Unexpected relative path: %v fs: %v", rel, fs.Name())
|
||||
}
|
||||
path = filepath.Join(user.HomeDir, "../..")
|
||||
rel = fs.GetRelativePath(path)
|
||||
if rel != "/" {
|
||||
t.Errorf("Unexpected relative path: %v path: %v", rel, path)
|
||||
t.Errorf("Unexpected relative path: %v path: %v fs: %v", rel, path, fs.Name())
|
||||
}
|
||||
path = filepath.Join(user.HomeDir, "../../../../../")
|
||||
rel = fs.GetRelativePath(path)
|
||||
if rel != "/" {
|
||||
t.Errorf("Unexpected relative path: %v", rel)
|
||||
t.Errorf("Unexpected relative path: %v fs: %v", rel, fs.Name())
|
||||
}
|
||||
path = filepath.Join(user.HomeDir, "/..")
|
||||
rel = fs.GetRelativePath(path)
|
||||
if rel != "/" {
|
||||
t.Errorf("Unexpected relative path: %v path: %v", rel, path)
|
||||
t.Errorf("Unexpected relative path: %v path: %v fs: %v", rel, path, fs.Name())
|
||||
}
|
||||
path = filepath.Join(user.HomeDir, "/../../../..")
|
||||
rel = fs.GetRelativePath(path)
|
||||
if rel != "/" {
|
||||
t.Errorf("Unexpected relative path: %v", rel)
|
||||
t.Errorf("Unexpected relative path: %v fs: %v", rel, fs.Name())
|
||||
}
|
||||
path = filepath.Join(user.HomeDir, "")
|
||||
rel = fs.GetRelativePath(path)
|
||||
if rel != "/" {
|
||||
t.Errorf("Unexpected relative path: %v", rel)
|
||||
t.Errorf("Unexpected relative path: %v fs: %v", rel, fs.Name())
|
||||
}
|
||||
path = filepath.Join(user.HomeDir, ".")
|
||||
rel = fs.GetRelativePath(path)
|
||||
if rel != "/" {
|
||||
t.Errorf("Unexpected relative path: %v", rel)
|
||||
t.Errorf("Unexpected relative path: %v fs: %v", rel, fs.Name())
|
||||
}
|
||||
path = filepath.Join(user.HomeDir, "somedir")
|
||||
rel = fs.GetRelativePath(path)
|
||||
if rel != "/somedir" {
|
||||
t.Errorf("Unexpected relative path: %v", rel)
|
||||
t.Errorf("Unexpected relative path: %v fs: %v", rel, fs.Name())
|
||||
}
|
||||
path = filepath.Join(user.HomeDir, "/somedir/subdir")
|
||||
rel = fs.GetRelativePath(path)
|
||||
if rel != "/somedir/subdir" {
|
||||
t.Errorf("Unexpected relative path: %v", rel)
|
||||
t.Errorf("Unexpected relative path: %v fs: %v", rel, fs.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue