portable.go 20 KB

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