portable.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. // Copyright (C) 2019-2023 Nicola Murino
  2. //
  3. // This program is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU Affero General Public License as published
  5. // by the Free Software Foundation, version 3.
  6. //
  7. // This program is distributed in the hope that it will be useful,
  8. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. // GNU Affero General Public License for more details.
  11. //
  12. // You should have received a copy of the GNU Affero General Public License
  13. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. //go:build !noportable
  15. // +build !noportable
  16. package cmd
  17. import (
  18. "fmt"
  19. "os"
  20. "path"
  21. "path/filepath"
  22. "strings"
  23. "github.com/sftpgo/sdk"
  24. "github.com/spf13/cobra"
  25. "github.com/drakkan/sftpgo/v2/internal/common"
  26. "github.com/drakkan/sftpgo/v2/internal/dataprovider"
  27. "github.com/drakkan/sftpgo/v2/internal/kms"
  28. "github.com/drakkan/sftpgo/v2/internal/service"
  29. "github.com/drakkan/sftpgo/v2/internal/sftpd"
  30. "github.com/drakkan/sftpgo/v2/internal/version"
  31. "github.com/drakkan/sftpgo/v2/internal/vfs"
  32. )
  33. var (
  34. directoryToServe string
  35. portableSFTPDPort int
  36. portableUsername string
  37. portablePassword string
  38. portablePasswordFile string
  39. portableStartDir string
  40. portableLogFile string
  41. portableLogLevel string
  42. portableLogUTCTime bool
  43. portablePublicKeys []string
  44. portablePermissions []string
  45. portableSSHCommands []string
  46. portableAllowedPatterns []string
  47. portableDeniedPatterns []string
  48. portableFsProvider string
  49. portableS3Bucket string
  50. portableS3Region string
  51. portableS3AccessKey string
  52. portableS3AccessSecret string
  53. portableS3RoleARN string
  54. portableS3Endpoint string
  55. portableS3StorageClass string
  56. portableS3ACL string
  57. portableS3KeyPrefix string
  58. portableS3ULPartSize int
  59. portableS3ULConcurrency int
  60. portableS3ForcePathStyle bool
  61. portableGCSBucket string
  62. portableGCSCredentialsFile string
  63. portableGCSAutoCredentials int
  64. portableGCSStorageClass string
  65. portableGCSKeyPrefix string
  66. portableFTPDPort int
  67. portableFTPSCert string
  68. portableFTPSKey string
  69. portableWebDAVPort int
  70. portableWebDAVCert string
  71. portableWebDAVKey string
  72. portableAzContainer string
  73. portableAzAccountName string
  74. portableAzAccountKey string
  75. portableAzEndpoint string
  76. portableAzAccessTier string
  77. portableAzSASURL string
  78. portableAzKeyPrefix string
  79. portableAzULPartSize int
  80. portableAzULConcurrency int
  81. portableAzDLPartSize int
  82. portableAzDLConcurrency int
  83. portableAzUseEmulator bool
  84. portableCryptPassphrase string
  85. portableSFTPEndpoint string
  86. portableSFTPUsername string
  87. portableSFTPPassword string
  88. portableSFTPPrivateKeyPath string
  89. portableSFTPFingerprints []string
  90. portableSFTPPrefix string
  91. portableSFTPDisableConcurrentReads bool
  92. portableSFTPDBufferSize int64
  93. portableCmd = &cobra.Command{
  94. Use: "portable",
  95. Short: "Serve a single directory/account",
  96. Long: `To serve the current working directory with auto generated credentials simply
  97. use:
  98. $ sftpgo portable
  99. Please take a look at the usage below to customize the serving parameters`,
  100. Run: func(_ *cobra.Command, _ []string) {
  101. portableDir := directoryToServe
  102. fsProvider := sdk.GetProviderByName(portableFsProvider)
  103. if !filepath.IsAbs(portableDir) {
  104. if fsProvider == sdk.LocalFilesystemProvider {
  105. portableDir, _ = filepath.Abs(portableDir)
  106. } else {
  107. portableDir = os.TempDir()
  108. }
  109. }
  110. permissions := make(map[string][]string)
  111. permissions["/"] = portablePermissions
  112. portableGCSCredentials := ""
  113. if fsProvider == sdk.GCSFilesystemProvider && portableGCSCredentialsFile != "" {
  114. contents, err := getFileContents(portableGCSCredentialsFile)
  115. if err != nil {
  116. fmt.Printf("Unable to get GCS credentials: %v\n", err)
  117. os.Exit(1)
  118. }
  119. portableGCSCredentials = contents
  120. portableGCSAutoCredentials = 0
  121. }
  122. portableSFTPPrivateKey := ""
  123. if fsProvider == sdk.SFTPFilesystemProvider && portableSFTPPrivateKeyPath != "" {
  124. contents, err := getFileContents(portableSFTPPrivateKeyPath)
  125. if err != nil {
  126. fmt.Printf("Unable to get SFTP private key: %v\n", err)
  127. os.Exit(1)
  128. }
  129. portableSFTPPrivateKey = contents
  130. }
  131. if portableFTPDPort >= 0 && portableFTPSCert != "" && portableFTPSKey != "" {
  132. keyPairs := []common.TLSKeyPair{
  133. {
  134. Cert: portableFTPSCert,
  135. Key: portableFTPSKey,
  136. ID: common.DefaultTLSKeyPaidID,
  137. },
  138. }
  139. _, err := common.NewCertManager(keyPairs, filepath.Clean(defaultConfigDir),
  140. "FTP portable")
  141. if err != nil {
  142. fmt.Printf("Unable to load FTPS key pair, cert file %q key file %q error: %v\n",
  143. portableFTPSCert, portableFTPSKey, err)
  144. os.Exit(1)
  145. }
  146. }
  147. if portableWebDAVPort > 0 && portableWebDAVCert != "" && portableWebDAVKey != "" {
  148. keyPairs := []common.TLSKeyPair{
  149. {
  150. Cert: portableWebDAVCert,
  151. Key: portableWebDAVKey,
  152. ID: common.DefaultTLSKeyPaidID,
  153. },
  154. }
  155. _, err := common.NewCertManager(keyPairs, filepath.Clean(defaultConfigDir),
  156. "WebDAV portable")
  157. if err != nil {
  158. fmt.Printf("Unable to load WebDAV key pair, cert file %q key file %q error: %v\n",
  159. portableWebDAVCert, portableWebDAVKey, err)
  160. os.Exit(1)
  161. }
  162. }
  163. pwd := portablePassword
  164. if portablePasswordFile != "" {
  165. content, err := os.ReadFile(portablePasswordFile)
  166. if err != nil {
  167. fmt.Printf("Unable to read password file %q: %v", portablePasswordFile, err)
  168. os.Exit(1)
  169. }
  170. pwd = strings.TrimSpace(string(content))
  171. }
  172. service.SetGraceTime(graceTime)
  173. service := service.Service{
  174. ConfigDir: filepath.Clean(defaultConfigDir),
  175. ConfigFile: defaultConfigFile,
  176. LogFilePath: portableLogFile,
  177. LogMaxSize: defaultLogMaxSize,
  178. LogMaxBackups: defaultLogMaxBackup,
  179. LogMaxAge: defaultLogMaxAge,
  180. LogCompress: defaultLogCompress,
  181. LogLevel: portableLogLevel,
  182. LogUTCTime: portableLogUTCTime,
  183. Shutdown: make(chan bool),
  184. PortableMode: 1,
  185. PortableUser: dataprovider.User{
  186. BaseUser: sdk.BaseUser{
  187. Username: portableUsername,
  188. Password: pwd,
  189. PublicKeys: portablePublicKeys,
  190. Permissions: permissions,
  191. HomeDir: portableDir,
  192. Status: 1,
  193. },
  194. Filters: dataprovider.UserFilters{
  195. BaseUserFilters: sdk.BaseUserFilters{
  196. FilePatterns: parsePatternsFilesFilters(),
  197. StartDirectory: portableStartDir,
  198. },
  199. },
  200. FsConfig: vfs.Filesystem{
  201. Provider: sdk.GetProviderByName(portableFsProvider),
  202. S3Config: vfs.S3FsConfig{
  203. BaseS3FsConfig: sdk.BaseS3FsConfig{
  204. Bucket: portableS3Bucket,
  205. Region: portableS3Region,
  206. AccessKey: portableS3AccessKey,
  207. RoleARN: portableS3RoleARN,
  208. Endpoint: portableS3Endpoint,
  209. StorageClass: portableS3StorageClass,
  210. ACL: portableS3ACL,
  211. KeyPrefix: portableS3KeyPrefix,
  212. UploadPartSize: int64(portableS3ULPartSize),
  213. UploadConcurrency: portableS3ULConcurrency,
  214. ForcePathStyle: portableS3ForcePathStyle,
  215. },
  216. AccessSecret: kms.NewPlainSecret(portableS3AccessSecret),
  217. },
  218. GCSConfig: vfs.GCSFsConfig{
  219. BaseGCSFsConfig: sdk.BaseGCSFsConfig{
  220. Bucket: portableGCSBucket,
  221. AutomaticCredentials: portableGCSAutoCredentials,
  222. StorageClass: portableGCSStorageClass,
  223. KeyPrefix: portableGCSKeyPrefix,
  224. },
  225. Credentials: kms.NewPlainSecret(portableGCSCredentials),
  226. },
  227. AzBlobConfig: vfs.AzBlobFsConfig{
  228. BaseAzBlobFsConfig: sdk.BaseAzBlobFsConfig{
  229. Container: portableAzContainer,
  230. AccountName: portableAzAccountName,
  231. Endpoint: portableAzEndpoint,
  232. AccessTier: portableAzAccessTier,
  233. KeyPrefix: portableAzKeyPrefix,
  234. UseEmulator: portableAzUseEmulator,
  235. UploadPartSize: int64(portableAzULPartSize),
  236. UploadConcurrency: portableAzULConcurrency,
  237. DownloadPartSize: int64(portableAzDLPartSize),
  238. DownloadConcurrency: portableAzDLConcurrency,
  239. },
  240. AccountKey: kms.NewPlainSecret(portableAzAccountKey),
  241. SASURL: kms.NewPlainSecret(portableAzSASURL),
  242. },
  243. CryptConfig: vfs.CryptFsConfig{
  244. Passphrase: kms.NewPlainSecret(portableCryptPassphrase),
  245. },
  246. SFTPConfig: vfs.SFTPFsConfig{
  247. BaseSFTPFsConfig: sdk.BaseSFTPFsConfig{
  248. Endpoint: portableSFTPEndpoint,
  249. Username: portableSFTPUsername,
  250. Fingerprints: portableSFTPFingerprints,
  251. Prefix: portableSFTPPrefix,
  252. DisableCouncurrentReads: portableSFTPDisableConcurrentReads,
  253. BufferSize: portableSFTPDBufferSize,
  254. },
  255. Password: kms.NewPlainSecret(portableSFTPPassword),
  256. PrivateKey: kms.NewPlainSecret(portableSFTPPrivateKey),
  257. },
  258. },
  259. },
  260. }
  261. err := service.StartPortableMode(portableSFTPDPort, portableFTPDPort, portableWebDAVPort, portableSSHCommands,
  262. portableFTPSCert, portableFTPSKey, portableWebDAVCert,
  263. portableWebDAVKey)
  264. if err == nil {
  265. service.Wait()
  266. if service.Error == nil {
  267. os.Exit(0)
  268. }
  269. }
  270. os.Exit(1)
  271. },
  272. }
  273. )
  274. func init() {
  275. version.AddFeature("+portable")
  276. portableCmd.Flags().StringVarP(&directoryToServe, "directory", "d", ".", `Path to the directory to serve.
  277. This can be an absolute path or a path
  278. relative to the current directory
  279. `)
  280. portableCmd.Flags().StringVar(&portableStartDir, "start-directory", "/", `Alternate start directory.
  281. This is a virtual path not a filesystem
  282. path`)
  283. portableCmd.Flags().IntVarP(&portableSFTPDPort, "sftpd-port", "s", 0, `0 means a random unprivileged port,
  284. < 0 disabled`)
  285. portableCmd.Flags().IntVar(&portableFTPDPort, "ftpd-port", -1, `0 means a random unprivileged port,
  286. < 0 disabled`)
  287. portableCmd.Flags().IntVar(&portableWebDAVPort, "webdav-port", -1, `0 means a random unprivileged port,
  288. < 0 disabled`)
  289. portableCmd.Flags().StringSliceVarP(&portableSSHCommands, "ssh-commands", "c", sftpd.GetDefaultSSHCommands(),
  290. `SSH commands to enable.
  291. "*" means any supported SSH command
  292. including scp
  293. `)
  294. portableCmd.Flags().StringVarP(&portableUsername, "username", "u", "", `Leave empty to use an auto generated
  295. value`)
  296. portableCmd.Flags().StringVarP(&portablePassword, "password", "p", "", `Leave empty to use an auto generated
  297. value`)
  298. portableCmd.Flags().StringVar(&portablePasswordFile, "password-file", "", `Read the password from the specified
  299. file path. Leave empty to use an auto
  300. generated value`)
  301. portableCmd.Flags().StringVarP(&portableLogFile, logFilePathFlag, "l", "", "Leave empty to disable logging")
  302. portableCmd.Flags().StringVar(&portableLogLevel, logLevelFlag, defaultLogLevel, `Set the log level.
  303. Supported values:
  304. debug, info, warn, error.
  305. `)
  306. portableCmd.Flags().BoolVar(&portableLogUTCTime, logUTCTimeFlag, false, "Use UTC time for logging")
  307. portableCmd.Flags().StringSliceVarP(&portablePublicKeys, "public-key", "k", []string{}, "")
  308. portableCmd.Flags().StringSliceVarP(&portablePermissions, "permissions", "g", []string{"list", "download"},
  309. `User's permissions. "*" means any
  310. permission`)
  311. portableCmd.Flags().StringArrayVar(&portableAllowedPatterns, "allowed-patterns", []string{},
  312. `Allowed file patterns case insensitive.
  313. The format is:
  314. /dir::pattern1,pattern2.
  315. For example: "/somedir::*.jpg,a*b?.png"`)
  316. portableCmd.Flags().StringArrayVar(&portableDeniedPatterns, "denied-patterns", []string{},
  317. `Denied file patterns case insensitive.
  318. The format is:
  319. /dir::pattern1,pattern2.
  320. For example: "/somedir::*.jpg,a*b?.png"`)
  321. portableCmd.Flags().StringVarP(&portableFsProvider, "fs-provider", "f", "osfs", `osfs => local filesystem (legacy value: 0)
  322. s3fs => AWS S3 compatible (legacy: 1)
  323. gcsfs => Google Cloud Storage (legacy: 2)
  324. azblobfs => Azure Blob Storage (legacy: 3)
  325. cryptfs => Encrypted local filesystem (legacy: 4)
  326. sftpfs => SFTP (legacy: 5)`)
  327. portableCmd.Flags().StringVar(&portableS3Bucket, "s3-bucket", "", "")
  328. portableCmd.Flags().StringVar(&portableS3Region, "s3-region", "", "")
  329. portableCmd.Flags().StringVar(&portableS3AccessKey, "s3-access-key", "", "")
  330. portableCmd.Flags().StringVar(&portableS3AccessSecret, "s3-access-secret", "", "")
  331. portableCmd.Flags().StringVar(&portableS3RoleARN, "s3-role-arn", "", "")
  332. portableCmd.Flags().StringVar(&portableS3Endpoint, "s3-endpoint", "", "")
  333. portableCmd.Flags().StringVar(&portableS3StorageClass, "s3-storage-class", "", "")
  334. portableCmd.Flags().StringVar(&portableS3ACL, "s3-acl", "", "")
  335. portableCmd.Flags().StringVar(&portableS3KeyPrefix, "s3-key-prefix", "", `Allows to restrict access to the
  336. virtual folder identified by this
  337. prefix and its contents`)
  338. portableCmd.Flags().IntVar(&portableS3ULPartSize, "s3-upload-part-size", 5, `The buffer size for multipart uploads
  339. (MB)`)
  340. portableCmd.Flags().IntVar(&portableS3ULConcurrency, "s3-upload-concurrency", 2, `How many parts are uploaded in
  341. parallel`)
  342. portableCmd.Flags().BoolVar(&portableS3ForcePathStyle, "s3-force-path-style", false, `Force path style bucket URL`)
  343. portableCmd.Flags().StringVar(&portableGCSBucket, "gcs-bucket", "", "")
  344. portableCmd.Flags().StringVar(&portableGCSStorageClass, "gcs-storage-class", "", "")
  345. portableCmd.Flags().StringVar(&portableGCSKeyPrefix, "gcs-key-prefix", "", `Allows to restrict access to the
  346. virtual folder identified by this
  347. prefix and its contents`)
  348. portableCmd.Flags().StringVar(&portableGCSCredentialsFile, "gcs-credentials-file", "", `Google Cloud Storage JSON credentials
  349. file`)
  350. portableCmd.Flags().IntVar(&portableGCSAutoCredentials, "gcs-automatic-credentials", 1, `0 means explicit credentials using
  351. a JSON credentials file, 1 automatic
  352. `)
  353. portableCmd.Flags().StringVar(&portableFTPSCert, "ftpd-cert", "", "Path to the certificate file for FTPS")
  354. portableCmd.Flags().StringVar(&portableFTPSKey, "ftpd-key", "", "Path to the key file for FTPS")
  355. portableCmd.Flags().StringVar(&portableWebDAVCert, "webdav-cert", "", `Path to the certificate file for WebDAV
  356. over HTTPS`)
  357. portableCmd.Flags().StringVar(&portableWebDAVKey, "webdav-key", "", `Path to the key file for WebDAV over
  358. HTTPS`)
  359. portableCmd.Flags().StringVar(&portableAzContainer, "az-container", "", "")
  360. portableCmd.Flags().StringVar(&portableAzAccountName, "az-account-name", "", "")
  361. portableCmd.Flags().StringVar(&portableAzAccountKey, "az-account-key", "", "")
  362. portableCmd.Flags().StringVar(&portableAzSASURL, "az-sas-url", "", `Shared access signature URL`)
  363. portableCmd.Flags().StringVar(&portableAzEndpoint, "az-endpoint", "", `Leave empty to use the default:
  364. "blob.core.windows.net"`)
  365. portableCmd.Flags().StringVar(&portableAzAccessTier, "az-access-tier", "", `Leave empty to use the default
  366. container setting`)
  367. portableCmd.Flags().StringVar(&portableAzKeyPrefix, "az-key-prefix", "", `Allows to restrict access to the
  368. virtual folder identified by this
  369. prefix and its contents`)
  370. portableCmd.Flags().IntVar(&portableAzULPartSize, "az-upload-part-size", 5, `The buffer size for multipart uploads
  371. (MB)`)
  372. portableCmd.Flags().IntVar(&portableAzULConcurrency, "az-upload-concurrency", 5, `How many parts are uploaded in
  373. parallel`)
  374. portableCmd.Flags().IntVar(&portableAzDLPartSize, "az-download-part-size", 5, `The buffer size for multipart downloads
  375. (MB)`)
  376. portableCmd.Flags().IntVar(&portableAzDLConcurrency, "az-download-concurrency", 5, `How many parts are downloaded in
  377. parallel`)
  378. portableCmd.Flags().BoolVar(&portableAzUseEmulator, "az-use-emulator", false, "")
  379. portableCmd.Flags().StringVar(&portableCryptPassphrase, "crypto-passphrase", "", `Passphrase for encryption/decryption`)
  380. portableCmd.Flags().StringVar(&portableSFTPEndpoint, "sftp-endpoint", "", `SFTP endpoint as host:port for SFTP
  381. provider`)
  382. portableCmd.Flags().StringVar(&portableSFTPUsername, "sftp-username", "", `SFTP user for SFTP provider`)
  383. portableCmd.Flags().StringVar(&portableSFTPPassword, "sftp-password", "", `SFTP password for SFTP provider`)
  384. portableCmd.Flags().StringVar(&portableSFTPPrivateKeyPath, "sftp-key-path", "", `SFTP private key path for SFTP provider`)
  385. portableCmd.Flags().StringSliceVar(&portableSFTPFingerprints, "sftp-fingerprints", []string{}, `SFTP fingerprints to verify remote host
  386. key for SFTP provider`)
  387. portableCmd.Flags().StringVar(&portableSFTPPrefix, "sftp-prefix", "", `SFTP prefix allows restrict all
  388. operations to a given path within the
  389. remote SFTP server`)
  390. portableCmd.Flags().BoolVar(&portableSFTPDisableConcurrentReads, "sftp-disable-concurrent-reads", false, `Concurrent reads are safe to use and
  391. disabling them will degrade performance.
  392. Disable for read once servers`)
  393. portableCmd.Flags().Int64Var(&portableSFTPDBufferSize, "sftp-buffer-size", 0, `The size of the buffer (in MB) to use
  394. for transfers. By enabling buffering,
  395. the reads and writes, from/to the
  396. remote SFTP server, are split in
  397. multiple concurrent requests and this
  398. allows data to be transferred at a
  399. faster rate, over high latency networks,
  400. by overlapping round-trip times`)
  401. portableCmd.Flags().IntVar(&graceTime, graceTimeFlag, 0,
  402. `This grace time defines the number of
  403. seconds allowed for existing transfers
  404. to get completed before shutting down.
  405. A graceful shutdown is triggered by an
  406. interrupt signal.
  407. `)
  408. rootCmd.AddCommand(portableCmd)
  409. }
  410. func parsePatternsFilesFilters() []sdk.PatternsFilter {
  411. var patterns []sdk.PatternsFilter
  412. for _, val := range portableAllowedPatterns {
  413. p, exts := getPatternsFilterValues(strings.TrimSpace(val))
  414. if p != "" {
  415. patterns = append(patterns, sdk.PatternsFilter{
  416. Path: path.Clean(p),
  417. AllowedPatterns: exts,
  418. DeniedPatterns: []string{},
  419. })
  420. }
  421. }
  422. for _, val := range portableDeniedPatterns {
  423. p, exts := getPatternsFilterValues(strings.TrimSpace(val))
  424. if p != "" {
  425. found := false
  426. for index, e := range patterns {
  427. if path.Clean(e.Path) == path.Clean(p) {
  428. patterns[index].DeniedPatterns = append(patterns[index].DeniedPatterns, exts...)
  429. found = true
  430. break
  431. }
  432. }
  433. if !found {
  434. patterns = append(patterns, sdk.PatternsFilter{
  435. Path: path.Clean(p),
  436. AllowedPatterns: []string{},
  437. DeniedPatterns: exts,
  438. })
  439. }
  440. }
  441. }
  442. return patterns
  443. }
  444. func getPatternsFilterValues(value string) (string, []string) {
  445. if strings.Contains(value, "::") {
  446. dirExts := strings.Split(value, "::")
  447. if len(dirExts) > 1 {
  448. dir := strings.TrimSpace(dirExts[0])
  449. exts := []string{}
  450. for _, e := range strings.Split(dirExts[1], ",") {
  451. cleanedExt := strings.TrimSpace(e)
  452. if cleanedExt != "" {
  453. exts = append(exts, cleanedExt)
  454. }
  455. }
  456. if dir != "" && len(exts) > 0 {
  457. return dir, exts
  458. }
  459. }
  460. }
  461. return "", nil
  462. }
  463. func getFileContents(name string) (string, error) {
  464. fi, err := os.Stat(name)
  465. if err != nil {
  466. return "", err
  467. }
  468. if fi.Size() > 1048576 {
  469. return "", fmt.Errorf("%q is too big %v/1048576 bytes", name, fi.Size())
  470. }
  471. contents, err := os.ReadFile(name)
  472. if err != nil {
  473. return "", err
  474. }
  475. return string(contents), nil
  476. }