diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index b62a93de..180b4553 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -16,7 +16,8 @@ jobs: with: version: v1.27 - test-deploy-unix: + tests-upload-unix: + name: Run tests and upload build artifacts on Linux and macOS runs-on: ${{ matrix.os }} strategy: matrix: @@ -40,7 +41,7 @@ jobs: - name: Initialize data provider run: ./sftpgo initprovider - - name: Run tests using SQLite provider + - name: Run test cases using SQLite as data provider run: go test -v ./... -coverprofile=coverage.txt -covermode=atomic - name: Upload to Codecov @@ -63,7 +64,7 @@ jobs: name: sftpgo-${{ matrix.os }}-go${{ matrix.go }} path: output - - name: Run tests using bolt provider + - name: Run test cases using bolt as data provider if: ${{ matrix.os == 'ubuntu-latest' && matrix.go == '1.14' }} run: | rm -f sftpgo.db @@ -73,14 +74,15 @@ jobs: env: SFTPGO_DATA_PROVIDER__DRIVER: bolt - - name: Run tests using memory provider + - name: Run test cases using the memory data provider if: ${{ matrix.os == 'ubuntu-latest' && matrix.go == '1.14' }} run: go test -v ./... -covermode=atomic env: SFTPGO_DATA_PROVIDER__DRIVER: memory SFTPGO_DATA_PROVIDER__NAME: - test-deploy-windows: + tests-upload-windows: + name: Run tests and upload build artifact on Windows runs-on: windows-latest steps: - uses: actions/checkout@v2 @@ -116,7 +118,8 @@ jobs: name: sftpgo-windows path: output - test-postgresql: + tests-postgresql: + name: Run test cases using PostgreSQL as data provider runs-on: ubuntu-latest env: SFTPGO_DATA_PROVIDER__DRIVER: postgresql diff --git a/README.md b/README.md index 9cc98f79..233d77e5 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ Some Linux distro packages are available: - [sftpgo-bin](https://aur.archlinux.org/packages/sftpgo-bin/). This package follows stable releases downloading the prebuilt linux binary from GitHub. It does not require `git`, `gcc` and `go` to build. - [sftpgo-git](https://aur.archlinux.org/packages/sftpgo-git/). This package builds and installs the latest git master. It requires `git`, `gcc` and `go` to build. +You can easily test new features selecting a commit from the [Actions](./actions) page and downloading the matching build artifacts for Linux, macOS or Windows. GitHub stores artifacts for 90 days. + Alternately, you can [build from source](./docs/build-from-source.md). ## Configuration diff --git a/cmd/reload_windows.go b/cmd/reload_windows.go index d6a83757..7e93a14a 100644 --- a/cmd/reload_windows.go +++ b/cmd/reload_windows.go @@ -21,10 +21,10 @@ var ( } err := s.Reload() if err != nil { - fmt.Printf("Error reloading service: %v\r\n", err) + fmt.Printf("Error sending reload signal: %v\r\n", err) os.Exit(1) } else { - fmt.Printf("Service reloaded!\r\n") + fmt.Printf("Reload signal sent!\r\n") } }, } diff --git a/cmd/rotatelogs_windows.go b/cmd/rotatelogs_windows.go new file mode 100644 index 00000000..3812808c --- /dev/null +++ b/cmd/rotatelogs_windows.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/drakkan/sftpgo/service" +) + +var ( + rotateLogCmd = &cobra.Command{ + Use: "rotatelogs", + Short: "Signal to the running service to close the existing log file and immediately create a new one", + Run: func(cmd *cobra.Command, args []string) { + s := service.WindowsService{ + Service: service.Service{ + Shutdown: make(chan bool), + }, + } + err := s.RotateLogFile() + if err != nil { + fmt.Printf("Error sending rotate log file signal to the service: %v\r\n", err) + os.Exit(1) + } else { + fmt.Printf("Rotate log file signal sent!\r\n") + } + }, + } +) + +func init() { + serviceCmd.AddCommand(rotateLogCmd) +} diff --git a/docs/account.md b/docs/account.md index 586f3dad..453060d5 100644 --- a/docs/account.md +++ b/docs/account.md @@ -13,7 +13,7 @@ For each account, the following properties can be configured: - `max_sessions` maximum concurrent sessions. 0 means unlimited. - `quota_size` maximum size allowed as bytes. 0 means unlimited. - `quota_files` maximum number of files allowed. 0 means unlimited. -- `permissions` the following per directory permissions are supported: +- `permissions` for SFTP paths. The following per directory permissions are supported: - `*` all permissions are granted - `list` list items is allowed - `download` download files is allowed diff --git a/docs/full-configuration.md b/docs/full-configuration.md index 25176711..d875d7c6 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -32,6 +32,8 @@ The `serve` command supports the following flags: - `--log-max-size` int. Maximum size in megabytes of the log file before it gets rotated. Default 10 or the value of `SFTPGO_LOG_MAX_SIZE` environment variable. It is unused if `log-file-path` is empty. - `--log-verbose` boolean. Enable verbose logs. Default `true` or the value of `SFTPGO_LOG_VERBOSE` environment variable (1 or `true`, 0 or `false`). +Log file can be rotated on demand sending a `SIGUSR1` signal on Unix based systems and using `sftpgo service rotatelogs` on Windows. + If you don't configure any private host key, the daemon will use `id_rsa` and `id_ecdsa` in the configuration directory. If these files don't exist, the daemon will attempt to autogenerate them (if the user that executes SFTPGo has write access to the `config-dir`). The server supports any private key format supported by [`crypto/ssh`](https://github.com/golang/crypto/blob/master/ssh/keys.go#L33). ## Configuration file diff --git a/docs/service.md b/docs/service.md index 9659a5f3..5b02b203 100644 --- a/docs/service.md +++ b/docs/service.md @@ -86,7 +86,8 @@ Usage: Available Commands: install Install SFTPGo as Windows Service - reload Reload the SFTPGo Windows Service sending a `paramchange` request + reload Reload the SFTPGo Windows Service sending a "paramchange" request + rotatelogs Signal to the running service to close the existing log file and immediately create a new one start Start SFTPGo Windows Service status Retrieve the status for the SFTPGo Windows Service stop Stop SFTPGo Windows Service diff --git a/init/sftpgo.service b/init/sftpgo.service index b3fa03d4..118720e5 100644 --- a/init/sftpgo.service +++ b/init/sftpgo.service @@ -1,5 +1,5 @@ [Unit] -Description=SFTPGo sftp server +Description=SFTPGo SFTP Server After=network.target [Service] diff --git a/logger/logger.go b/logger/logger.go index 01e82283..d6968304 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -9,6 +9,7 @@ package logger import ( + "errors" "fmt" "os" "path/filepath" @@ -37,6 +38,7 @@ const ( var ( logger zerolog.Logger consoleLogger zerolog.Logger + rollingLogger *lumberjack.Logger ) // GetLogger get the configured logger instance @@ -48,13 +50,14 @@ func GetLogger() *zerolog.Logger { func InitLogger(logFilePath string, logMaxSize int, logMaxBackups int, logMaxAge int, logCompress bool, level zerolog.Level) { zerolog.TimeFieldFormat = dateFormat if isLogFilePathValid(logFilePath) { - logger = zerolog.New(&lumberjack.Logger{ + rollingLogger = &lumberjack.Logger{ Filename: logFilePath, MaxSize: logMaxSize, MaxBackups: logMaxBackups, MaxAge: logMaxAge, Compress: logCompress, - }) + } + logger = zerolog.New(rollingLogger) EnableConsoleLogger(level) } else { logger = zerolog.New(logSyncWrapper{ @@ -69,6 +72,7 @@ func InitLogger(logFilePath string, logMaxSize int, logMaxBackups int, logMaxAge // ConsoleLogger will not be affected func DisableLogger() { logger = zerolog.Nop() + rollingLogger = nil } // EnableConsoleLogger enables the console logger @@ -81,6 +85,14 @@ func EnableConsoleLogger(level zerolog.Level) { consoleLogger = zerolog.New(consoleOutput).With().Timestamp().Logger().Level(level) } +// RotateLogFile closes the existing log file and immediately create a new one +func RotateLogFile() error { + if rollingLogger != nil { + return rollingLogger.Rotate() + } + return errors.New("logging to file is disabled") +} + // Log logs at the specified level for the specified sender func Log(level LogLevel, sender string, connectionID string, format string, v ...interface{}) { switch level { diff --git a/service/service.go b/service/service.go index 116af6ff..276a05a6 100644 --- a/service/service.go +++ b/service/service.go @@ -124,6 +124,7 @@ func (s *Service) Start() error { func (s *Service) Wait() { if s.PortableMode != 1 { registerSigHup() + registerSigUSR1() } <-s.Shutdown } diff --git a/service/service_windows.go b/service/service_windows.go index d82290e9..5b529f3f 100644 --- a/service/service_windows.go +++ b/service/service_windows.go @@ -17,8 +17,10 @@ import ( ) const ( - serviceName = "SFTPGo" - serviceDesc = "Full featured and highly configurable SFTP server" + serviceName = "SFTPGo" + serviceDesc = "Full featured and highly configurable SFTP server" + rotateLogCmd = svc.Cmd(128) + acceptRotateLog = svc.Accepted(rotateLogCmd) ) // Status defines service status @@ -63,7 +65,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 | svc.AcceptParamChange + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptParamChange | acceptRotateLog changes <- svc.Status{State: svc.StartPending} if err := s.Service.Start(); err != nil { return true, 1 @@ -91,6 +93,12 @@ loop: if err != nil { logger.Warn(logSender, "", "error reloading TLS certificate: %v", err) } + case rotateLogCmd: + logger.Debug(logSender, "", "Received log file rotation request") + err := logger.RotateLogFile() + if err != nil { + logger.Warn(logSender, "", "error rotating log file: %v", err) + } default: continue loop } @@ -157,6 +165,24 @@ func (s *WindowsService) Reload() error { return nil } +func (s *WindowsService) RotateLogFile() 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(rotateLogCmd) + if err != nil { + return fmt.Errorf("could not send control=%d: %v", rotateLogCmd, err) + } + return nil +} + func (s *WindowsService) Install(args ...string) error { exePath, err := s.getExePath() if err != nil { diff --git a/service/sighup_windows.go b/service/sighup_windows.go index f3199e2f..c95f0e60 100644 --- a/service/sighup_windows.go +++ b/service/sighup_windows.go @@ -1,4 +1,3 @@ package service -func registerSigHup() { -} +func registerSigHup() {} diff --git a/service/sigusr1_unix.go b/service/sigusr1_unix.go new file mode 100644 index 00000000..f6e87a52 --- /dev/null +++ b/service/sigusr1_unix.go @@ -0,0 +1,25 @@ +// +build !windows + +package service + +import ( + "os" + "os/signal" + "syscall" + + "github.com/drakkan/sftpgo/logger" +) + +func registerSigUSR1() { + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGUSR1) + go func() { + for range sig { + logger.Debug(logSender, "", "Received log file rotation request") + err := logger.RotateLogFile() + if err != nil { + logger.Warn(logSender, "", "error rotating log file: %v", err) + } + } + }() +} diff --git a/service/sigusr1_windows.go b/service/sigusr1_windows.go new file mode 100644 index 00000000..c1214303 --- /dev/null +++ b/service/sigusr1_windows.go @@ -0,0 +1,3 @@ +package service + +func registerSigUSR1() {}