mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-22 23:50:32 +00:00
dba088daed
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
490 lines
20 KiB
Go
490 lines
20 KiB
Go
// Copyright (C) 2019-2023 Nicola Murino
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published
|
|
// by the Free Software Foundation, version 3.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
//go:build !noportable
|
|
// +build !noportable
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/sftpgo/sdk"
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/drakkan/sftpgo/v2/internal/common"
|
|
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
|
"github.com/drakkan/sftpgo/v2/internal/kms"
|
|
"github.com/drakkan/sftpgo/v2/internal/service"
|
|
"github.com/drakkan/sftpgo/v2/internal/sftpd"
|
|
"github.com/drakkan/sftpgo/v2/internal/version"
|
|
"github.com/drakkan/sftpgo/v2/internal/vfs"
|
|
)
|
|
|
|
var (
|
|
directoryToServe string
|
|
portableSFTPDPort int
|
|
portableUsername string
|
|
portablePassword string
|
|
portablePasswordFile string
|
|
portableStartDir string
|
|
portableLogFile string
|
|
portableLogLevel string
|
|
portableLogUTCTime bool
|
|
portablePublicKeys []string
|
|
portablePermissions []string
|
|
portableSSHCommands []string
|
|
portableAllowedPatterns []string
|
|
portableDeniedPatterns []string
|
|
portableFsProvider string
|
|
portableS3Bucket string
|
|
portableS3Region string
|
|
portableS3AccessKey string
|
|
portableS3AccessSecret string
|
|
portableS3RoleARN string
|
|
portableS3Endpoint string
|
|
portableS3StorageClass string
|
|
portableS3ACL string
|
|
portableS3KeyPrefix string
|
|
portableS3ULPartSize int
|
|
portableS3ULConcurrency int
|
|
portableS3ForcePathStyle bool
|
|
portableGCSBucket string
|
|
portableGCSCredentialsFile string
|
|
portableGCSAutoCredentials int
|
|
portableGCSStorageClass string
|
|
portableGCSKeyPrefix string
|
|
portableFTPDPort int
|
|
portableFTPSCert string
|
|
portableFTPSKey string
|
|
portableWebDAVPort int
|
|
portableWebDAVCert string
|
|
portableWebDAVKey string
|
|
portableAzContainer string
|
|
portableAzAccountName string
|
|
portableAzAccountKey string
|
|
portableAzEndpoint string
|
|
portableAzAccessTier string
|
|
portableAzSASURL string
|
|
portableAzKeyPrefix string
|
|
portableAzULPartSize int
|
|
portableAzULConcurrency int
|
|
portableAzDLPartSize int
|
|
portableAzDLConcurrency int
|
|
portableAzUseEmulator bool
|
|
portableCryptPassphrase string
|
|
portableSFTPEndpoint string
|
|
portableSFTPUsername string
|
|
portableSFTPPassword string
|
|
portableSFTPPrivateKeyPath string
|
|
portableSFTPFingerprints []string
|
|
portableSFTPPrefix string
|
|
portableSFTPDisableConcurrentReads bool
|
|
portableSFTPDBufferSize int64
|
|
portableCmd = &cobra.Command{
|
|
Use: "portable",
|
|
Short: "Serve a single directory/account",
|
|
Long: `To serve the current working directory with auto generated credentials simply
|
|
use:
|
|
|
|
$ sftpgo portable
|
|
|
|
Please take a look at the usage below to customize the serving parameters`,
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
portableDir := directoryToServe
|
|
fsProvider := sdk.GetProviderByName(portableFsProvider)
|
|
if !filepath.IsAbs(portableDir) {
|
|
if fsProvider == sdk.LocalFilesystemProvider {
|
|
portableDir, _ = filepath.Abs(portableDir)
|
|
} else {
|
|
portableDir = os.TempDir()
|
|
}
|
|
}
|
|
permissions := make(map[string][]string)
|
|
permissions["/"] = portablePermissions
|
|
portableGCSCredentials := ""
|
|
if fsProvider == sdk.GCSFilesystemProvider && portableGCSCredentialsFile != "" {
|
|
contents, err := getFileContents(portableGCSCredentialsFile)
|
|
if err != nil {
|
|
fmt.Printf("Unable to get GCS credentials: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
portableGCSCredentials = contents
|
|
portableGCSAutoCredentials = 0
|
|
}
|
|
portableSFTPPrivateKey := ""
|
|
if fsProvider == sdk.SFTPFilesystemProvider && portableSFTPPrivateKeyPath != "" {
|
|
contents, err := getFileContents(portableSFTPPrivateKeyPath)
|
|
if err != nil {
|
|
fmt.Printf("Unable to get SFTP private key: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
portableSFTPPrivateKey = contents
|
|
}
|
|
if portableFTPDPort >= 0 && portableFTPSCert != "" && portableFTPSKey != "" {
|
|
keyPairs := []common.TLSKeyPair{
|
|
{
|
|
Cert: portableFTPSCert,
|
|
Key: portableFTPSKey,
|
|
ID: common.DefaultTLSKeyPaidID,
|
|
},
|
|
}
|
|
_, err := common.NewCertManager(keyPairs, filepath.Clean(defaultConfigDir),
|
|
"FTP portable")
|
|
if err != nil {
|
|
fmt.Printf("Unable to load FTPS key pair, cert file %q key file %q error: %v\n",
|
|
portableFTPSCert, portableFTPSKey, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
if portableWebDAVPort > 0 && portableWebDAVCert != "" && portableWebDAVKey != "" {
|
|
keyPairs := []common.TLSKeyPair{
|
|
{
|
|
Cert: portableWebDAVCert,
|
|
Key: portableWebDAVKey,
|
|
ID: common.DefaultTLSKeyPaidID,
|
|
},
|
|
}
|
|
_, err := common.NewCertManager(keyPairs, filepath.Clean(defaultConfigDir),
|
|
"WebDAV portable")
|
|
if err != nil {
|
|
fmt.Printf("Unable to load WebDAV key pair, cert file %q key file %q error: %v\n",
|
|
portableWebDAVCert, portableWebDAVKey, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
pwd := portablePassword
|
|
if portablePasswordFile != "" {
|
|
content, err := os.ReadFile(portablePasswordFile)
|
|
if err != nil {
|
|
fmt.Printf("Unable to read password file %q: %v", portablePasswordFile, err)
|
|
os.Exit(1)
|
|
}
|
|
pwd = strings.TrimSpace(string(content))
|
|
}
|
|
service.SetGraceTime(graceTime)
|
|
service := service.Service{
|
|
ConfigDir: filepath.Clean(defaultConfigDir),
|
|
ConfigFile: defaultConfigFile,
|
|
LogFilePath: portableLogFile,
|
|
LogMaxSize: defaultLogMaxSize,
|
|
LogMaxBackups: defaultLogMaxBackup,
|
|
LogMaxAge: defaultLogMaxAge,
|
|
LogCompress: defaultLogCompress,
|
|
LogLevel: portableLogLevel,
|
|
LogUTCTime: portableLogUTCTime,
|
|
Shutdown: make(chan bool),
|
|
PortableMode: 1,
|
|
PortableUser: dataprovider.User{
|
|
BaseUser: sdk.BaseUser{
|
|
Username: portableUsername,
|
|
Password: pwd,
|
|
PublicKeys: portablePublicKeys,
|
|
Permissions: permissions,
|
|
HomeDir: portableDir,
|
|
Status: 1,
|
|
},
|
|
Filters: dataprovider.UserFilters{
|
|
BaseUserFilters: sdk.BaseUserFilters{
|
|
FilePatterns: parsePatternsFilesFilters(),
|
|
StartDirectory: portableStartDir,
|
|
},
|
|
},
|
|
FsConfig: vfs.Filesystem{
|
|
Provider: sdk.GetProviderByName(portableFsProvider),
|
|
S3Config: vfs.S3FsConfig{
|
|
BaseS3FsConfig: sdk.BaseS3FsConfig{
|
|
Bucket: portableS3Bucket,
|
|
Region: portableS3Region,
|
|
AccessKey: portableS3AccessKey,
|
|
RoleARN: portableS3RoleARN,
|
|
Endpoint: portableS3Endpoint,
|
|
StorageClass: portableS3StorageClass,
|
|
ACL: portableS3ACL,
|
|
KeyPrefix: portableS3KeyPrefix,
|
|
UploadPartSize: int64(portableS3ULPartSize),
|
|
UploadConcurrency: portableS3ULConcurrency,
|
|
ForcePathStyle: portableS3ForcePathStyle,
|
|
},
|
|
AccessSecret: kms.NewPlainSecret(portableS3AccessSecret),
|
|
},
|
|
GCSConfig: vfs.GCSFsConfig{
|
|
BaseGCSFsConfig: sdk.BaseGCSFsConfig{
|
|
Bucket: portableGCSBucket,
|
|
AutomaticCredentials: portableGCSAutoCredentials,
|
|
StorageClass: portableGCSStorageClass,
|
|
KeyPrefix: portableGCSKeyPrefix,
|
|
},
|
|
Credentials: kms.NewPlainSecret(portableGCSCredentials),
|
|
},
|
|
AzBlobConfig: vfs.AzBlobFsConfig{
|
|
BaseAzBlobFsConfig: sdk.BaseAzBlobFsConfig{
|
|
Container: portableAzContainer,
|
|
AccountName: portableAzAccountName,
|
|
Endpoint: portableAzEndpoint,
|
|
AccessTier: portableAzAccessTier,
|
|
KeyPrefix: portableAzKeyPrefix,
|
|
UseEmulator: portableAzUseEmulator,
|
|
UploadPartSize: int64(portableAzULPartSize),
|
|
UploadConcurrency: portableAzULConcurrency,
|
|
DownloadPartSize: int64(portableAzDLPartSize),
|
|
DownloadConcurrency: portableAzDLConcurrency,
|
|
},
|
|
AccountKey: kms.NewPlainSecret(portableAzAccountKey),
|
|
SASURL: kms.NewPlainSecret(portableAzSASURL),
|
|
},
|
|
CryptConfig: vfs.CryptFsConfig{
|
|
Passphrase: kms.NewPlainSecret(portableCryptPassphrase),
|
|
},
|
|
SFTPConfig: vfs.SFTPFsConfig{
|
|
BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
|
|
Endpoint: portableSFTPEndpoint,
|
|
Username: portableSFTPUsername,
|
|
Fingerprints: portableSFTPFingerprints,
|
|
Prefix: portableSFTPPrefix,
|
|
DisableCouncurrentReads: portableSFTPDisableConcurrentReads,
|
|
BufferSize: portableSFTPDBufferSize,
|
|
},
|
|
Password: kms.NewPlainSecret(portableSFTPPassword),
|
|
PrivateKey: kms.NewPlainSecret(portableSFTPPrivateKey),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
err := service.StartPortableMode(portableSFTPDPort, portableFTPDPort, portableWebDAVPort, portableSSHCommands,
|
|
portableFTPSCert, portableFTPSKey, portableWebDAVCert,
|
|
portableWebDAVKey)
|
|
if err == nil {
|
|
service.Wait()
|
|
if service.Error == nil {
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
os.Exit(1)
|
|
},
|
|
}
|
|
)
|
|
|
|
func init() {
|
|
version.AddFeature("+portable")
|
|
|
|
portableCmd.Flags().StringVarP(&directoryToServe, "directory", "d", ".", `Path to the directory to serve.
|
|
This can be an absolute path or a path
|
|
relative to the current directory
|
|
`)
|
|
portableCmd.Flags().StringVar(&portableStartDir, "start-directory", "/", `Alternate start directory.
|
|
This is a virtual path not a filesystem
|
|
path`)
|
|
portableCmd.Flags().IntVarP(&portableSFTPDPort, "sftpd-port", "s", 0, `0 means a random unprivileged port,
|
|
< 0 disabled`)
|
|
portableCmd.Flags().IntVar(&portableFTPDPort, "ftpd-port", -1, `0 means a random unprivileged port,
|
|
< 0 disabled`)
|
|
portableCmd.Flags().IntVar(&portableWebDAVPort, "webdav-port", -1, `0 means a random unprivileged port,
|
|
< 0 disabled`)
|
|
portableCmd.Flags().StringSliceVarP(&portableSSHCommands, "ssh-commands", "c", sftpd.GetDefaultSSHCommands(),
|
|
`SSH commands to enable.
|
|
"*" means any supported SSH command
|
|
including scp
|
|
`)
|
|
portableCmd.Flags().StringVarP(&portableUsername, "username", "u", "", `Leave empty to use an auto generated
|
|
value`)
|
|
portableCmd.Flags().StringVarP(&portablePassword, "password", "p", "", `Leave empty to use an auto generated
|
|
value`)
|
|
portableCmd.Flags().StringVar(&portablePasswordFile, "password-file", "", `Read the password from the specified
|
|
file path. Leave empty to use an auto
|
|
generated value`)
|
|
portableCmd.Flags().StringVarP(&portableLogFile, logFilePathFlag, "l", "", "Leave empty to disable logging")
|
|
portableCmd.Flags().StringVar(&portableLogLevel, logLevelFlag, defaultLogLevel, `Set the log level.
|
|
Supported values:
|
|
|
|
debug, info, warn, error.
|
|
`)
|
|
portableCmd.Flags().BoolVar(&portableLogUTCTime, logUTCTimeFlag, false, "Use UTC time for logging")
|
|
portableCmd.Flags().StringSliceVarP(&portablePublicKeys, "public-key", "k", []string{}, "")
|
|
portableCmd.Flags().StringSliceVarP(&portablePermissions, "permissions", "g", []string{"list", "download"},
|
|
`User's permissions. "*" means any
|
|
permission`)
|
|
portableCmd.Flags().StringArrayVar(&portableAllowedPatterns, "allowed-patterns", []string{},
|
|
`Allowed file patterns case insensitive.
|
|
The format is:
|
|
/dir::pattern1,pattern2.
|
|
For example: "/somedir::*.jpg,a*b?.png"`)
|
|
portableCmd.Flags().StringArrayVar(&portableDeniedPatterns, "denied-patterns", []string{},
|
|
`Denied file patterns case insensitive.
|
|
The format is:
|
|
/dir::pattern1,pattern2.
|
|
For example: "/somedir::*.jpg,a*b?.png"`)
|
|
portableCmd.Flags().StringVarP(&portableFsProvider, "fs-provider", "f", "osfs", `osfs => local filesystem (legacy value: 0)
|
|
s3fs => AWS S3 compatible (legacy: 1)
|
|
gcsfs => Google Cloud Storage (legacy: 2)
|
|
azblobfs => Azure Blob Storage (legacy: 3)
|
|
cryptfs => Encrypted local filesystem (legacy: 4)
|
|
sftpfs => SFTP (legacy: 5)`)
|
|
portableCmd.Flags().StringVar(&portableS3Bucket, "s3-bucket", "", "")
|
|
portableCmd.Flags().StringVar(&portableS3Region, "s3-region", "", "")
|
|
portableCmd.Flags().StringVar(&portableS3AccessKey, "s3-access-key", "", "")
|
|
portableCmd.Flags().StringVar(&portableS3AccessSecret, "s3-access-secret", "", "")
|
|
portableCmd.Flags().StringVar(&portableS3RoleARN, "s3-role-arn", "", "")
|
|
portableCmd.Flags().StringVar(&portableS3Endpoint, "s3-endpoint", "", "")
|
|
portableCmd.Flags().StringVar(&portableS3StorageClass, "s3-storage-class", "", "")
|
|
portableCmd.Flags().StringVar(&portableS3ACL, "s3-acl", "", "")
|
|
portableCmd.Flags().StringVar(&portableS3KeyPrefix, "s3-key-prefix", "", `Allows to restrict access to the
|
|
virtual folder identified by this
|
|
prefix and its contents`)
|
|
portableCmd.Flags().IntVar(&portableS3ULPartSize, "s3-upload-part-size", 5, `The buffer size for multipart uploads
|
|
(MB)`)
|
|
portableCmd.Flags().IntVar(&portableS3ULConcurrency, "s3-upload-concurrency", 2, `How many parts are uploaded in
|
|
parallel`)
|
|
portableCmd.Flags().BoolVar(&portableS3ForcePathStyle, "s3-force-path-style", false, `Force path style bucket URL`)
|
|
portableCmd.Flags().StringVar(&portableGCSBucket, "gcs-bucket", "", "")
|
|
portableCmd.Flags().StringVar(&portableGCSStorageClass, "gcs-storage-class", "", "")
|
|
portableCmd.Flags().StringVar(&portableGCSKeyPrefix, "gcs-key-prefix", "", `Allows to restrict access to the
|
|
virtual folder identified by this
|
|
prefix and its contents`)
|
|
portableCmd.Flags().StringVar(&portableGCSCredentialsFile, "gcs-credentials-file", "", `Google Cloud Storage JSON credentials
|
|
file`)
|
|
portableCmd.Flags().IntVar(&portableGCSAutoCredentials, "gcs-automatic-credentials", 1, `0 means explicit credentials using
|
|
a JSON credentials file, 1 automatic
|
|
`)
|
|
portableCmd.Flags().StringVar(&portableFTPSCert, "ftpd-cert", "", "Path to the certificate file for FTPS")
|
|
portableCmd.Flags().StringVar(&portableFTPSKey, "ftpd-key", "", "Path to the key file for FTPS")
|
|
portableCmd.Flags().StringVar(&portableWebDAVCert, "webdav-cert", "", `Path to the certificate file for WebDAV
|
|
over HTTPS`)
|
|
portableCmd.Flags().StringVar(&portableWebDAVKey, "webdav-key", "", `Path to the key file for WebDAV over
|
|
HTTPS`)
|
|
portableCmd.Flags().StringVar(&portableAzContainer, "az-container", "", "")
|
|
portableCmd.Flags().StringVar(&portableAzAccountName, "az-account-name", "", "")
|
|
portableCmd.Flags().StringVar(&portableAzAccountKey, "az-account-key", "", "")
|
|
portableCmd.Flags().StringVar(&portableAzSASURL, "az-sas-url", "", `Shared access signature URL`)
|
|
portableCmd.Flags().StringVar(&portableAzEndpoint, "az-endpoint", "", `Leave empty to use the default:
|
|
"blob.core.windows.net"`)
|
|
portableCmd.Flags().StringVar(&portableAzAccessTier, "az-access-tier", "", `Leave empty to use the default
|
|
container setting`)
|
|
portableCmd.Flags().StringVar(&portableAzKeyPrefix, "az-key-prefix", "", `Allows to restrict access to the
|
|
virtual folder identified by this
|
|
prefix and its contents`)
|
|
portableCmd.Flags().IntVar(&portableAzULPartSize, "az-upload-part-size", 5, `The buffer size for multipart uploads
|
|
(MB)`)
|
|
portableCmd.Flags().IntVar(&portableAzULConcurrency, "az-upload-concurrency", 5, `How many parts are uploaded in
|
|
parallel`)
|
|
portableCmd.Flags().IntVar(&portableAzDLPartSize, "az-download-part-size", 5, `The buffer size for multipart downloads
|
|
(MB)`)
|
|
portableCmd.Flags().IntVar(&portableAzDLConcurrency, "az-download-concurrency", 5, `How many parts are downloaded in
|
|
parallel`)
|
|
portableCmd.Flags().BoolVar(&portableAzUseEmulator, "az-use-emulator", false, "")
|
|
portableCmd.Flags().StringVar(&portableCryptPassphrase, "crypto-passphrase", "", `Passphrase for encryption/decryption`)
|
|
portableCmd.Flags().StringVar(&portableSFTPEndpoint, "sftp-endpoint", "", `SFTP endpoint as host:port for SFTP
|
|
provider`)
|
|
portableCmd.Flags().StringVar(&portableSFTPUsername, "sftp-username", "", `SFTP user for SFTP provider`)
|
|
portableCmd.Flags().StringVar(&portableSFTPPassword, "sftp-password", "", `SFTP password for SFTP provider`)
|
|
portableCmd.Flags().StringVar(&portableSFTPPrivateKeyPath, "sftp-key-path", "", `SFTP private key path for SFTP provider`)
|
|
portableCmd.Flags().StringSliceVar(&portableSFTPFingerprints, "sftp-fingerprints", []string{}, `SFTP fingerprints to verify remote host
|
|
key for SFTP provider`)
|
|
portableCmd.Flags().StringVar(&portableSFTPPrefix, "sftp-prefix", "", `SFTP prefix allows restrict all
|
|
operations to a given path within the
|
|
remote SFTP server`)
|
|
portableCmd.Flags().BoolVar(&portableSFTPDisableConcurrentReads, "sftp-disable-concurrent-reads", false, `Concurrent reads are safe to use and
|
|
disabling them will degrade performance.
|
|
Disable for read once servers`)
|
|
portableCmd.Flags().Int64Var(&portableSFTPDBufferSize, "sftp-buffer-size", 0, `The size of the buffer (in MB) to use
|
|
for transfers. By enabling buffering,
|
|
the reads and writes, from/to the
|
|
remote SFTP server, are split in
|
|
multiple concurrent requests and this
|
|
allows data to be transferred at a
|
|
faster rate, over high latency networks,
|
|
by overlapping round-trip times`)
|
|
portableCmd.Flags().IntVar(&graceTime, graceTimeFlag, 0,
|
|
`This grace time defines the number of
|
|
seconds allowed for existing transfers
|
|
to get completed before shutting down.
|
|
A graceful shutdown is triggered by an
|
|
interrupt signal.
|
|
`)
|
|
rootCmd.AddCommand(portableCmd)
|
|
}
|
|
|
|
func parsePatternsFilesFilters() []sdk.PatternsFilter {
|
|
var patterns []sdk.PatternsFilter
|
|
for _, val := range portableAllowedPatterns {
|
|
p, exts := getPatternsFilterValues(strings.TrimSpace(val))
|
|
if p != "" {
|
|
patterns = append(patterns, sdk.PatternsFilter{
|
|
Path: path.Clean(p),
|
|
AllowedPatterns: exts,
|
|
DeniedPatterns: []string{},
|
|
})
|
|
}
|
|
}
|
|
for _, val := range portableDeniedPatterns {
|
|
p, exts := getPatternsFilterValues(strings.TrimSpace(val))
|
|
if p != "" {
|
|
found := false
|
|
for index, e := range patterns {
|
|
if path.Clean(e.Path) == path.Clean(p) {
|
|
patterns[index].DeniedPatterns = append(patterns[index].DeniedPatterns, exts...)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
patterns = append(patterns, sdk.PatternsFilter{
|
|
Path: path.Clean(p),
|
|
AllowedPatterns: []string{},
|
|
DeniedPatterns: exts,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return patterns
|
|
}
|
|
|
|
func getPatternsFilterValues(value string) (string, []string) {
|
|
if strings.Contains(value, "::") {
|
|
dirExts := strings.Split(value, "::")
|
|
if len(dirExts) > 1 {
|
|
dir := strings.TrimSpace(dirExts[0])
|
|
exts := []string{}
|
|
for _, e := range strings.Split(dirExts[1], ",") {
|
|
cleanedExt := strings.TrimSpace(e)
|
|
if cleanedExt != "" {
|
|
exts = append(exts, cleanedExt)
|
|
}
|
|
}
|
|
if dir != "" && len(exts) > 0 {
|
|
return dir, exts
|
|
}
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func getFileContents(name string) (string, error) {
|
|
fi, err := os.Stat(name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if fi.Size() > 1048576 {
|
|
return "", fmt.Errorf("%q is too big %v/1048576 bytes", name, fi.Size())
|
|
}
|
|
contents, err := os.ReadFile(name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(contents), nil
|
|
}
|