dataretention.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. // Copyright (C) 2019 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. package common
  15. import (
  16. "bytes"
  17. "context"
  18. "encoding/json"
  19. "errors"
  20. "fmt"
  21. "io"
  22. "net/http"
  23. "net/url"
  24. "os"
  25. "os/exec"
  26. "path"
  27. "path/filepath"
  28. "strings"
  29. "sync"
  30. "time"
  31. "github.com/wneessen/go-mail"
  32. "github.com/drakkan/sftpgo/v2/internal/command"
  33. "github.com/drakkan/sftpgo/v2/internal/dataprovider"
  34. "github.com/drakkan/sftpgo/v2/internal/httpclient"
  35. "github.com/drakkan/sftpgo/v2/internal/logger"
  36. "github.com/drakkan/sftpgo/v2/internal/smtp"
  37. "github.com/drakkan/sftpgo/v2/internal/util"
  38. "github.com/drakkan/sftpgo/v2/internal/vfs"
  39. )
  40. // RetentionCheckNotification defines the supported notification methods for a retention check result
  41. type RetentionCheckNotification = string
  42. // Supported notification methods
  43. const (
  44. // notify results using the defined "data_retention_hook"
  45. RetentionCheckNotificationHook = "Hook"
  46. // notify results by email
  47. RetentionCheckNotificationEmail = "Email"
  48. )
  49. var (
  50. // RetentionChecks is the list of active retention checks
  51. RetentionChecks ActiveRetentionChecks
  52. )
  53. // ActiveRetentionChecks holds the active retention checks
  54. type ActiveRetentionChecks struct {
  55. sync.RWMutex
  56. Checks []RetentionCheck
  57. }
  58. // Get returns the active retention checks
  59. func (c *ActiveRetentionChecks) Get(role string) []RetentionCheck {
  60. c.RLock()
  61. defer c.RUnlock()
  62. checks := make([]RetentionCheck, 0, len(c.Checks))
  63. for _, check := range c.Checks {
  64. if role == "" || role == check.Role {
  65. foldersCopy := make([]dataprovider.FolderRetention, len(check.Folders))
  66. copy(foldersCopy, check.Folders)
  67. notificationsCopy := make([]string, len(check.Notifications))
  68. copy(notificationsCopy, check.Notifications)
  69. checks = append(checks, RetentionCheck{
  70. Username: check.Username,
  71. StartTime: check.StartTime,
  72. Notifications: notificationsCopy,
  73. Email: check.Email,
  74. Folders: foldersCopy,
  75. })
  76. }
  77. }
  78. return checks
  79. }
  80. // Add a new retention check, returns nil if a retention check for the given
  81. // username is already active. The returned result can be used to start the check
  82. func (c *ActiveRetentionChecks) Add(check RetentionCheck, user *dataprovider.User) *RetentionCheck {
  83. c.Lock()
  84. defer c.Unlock()
  85. for _, val := range c.Checks {
  86. if val.Username == user.Username {
  87. return nil
  88. }
  89. }
  90. // we silently ignore file patterns
  91. user.Filters.FilePatterns = nil
  92. conn := NewBaseConnection("", "", "", "", *user)
  93. conn.SetProtocol(ProtocolDataRetention)
  94. conn.ID = fmt.Sprintf("data_retention_%v", user.Username)
  95. check.Username = user.Username
  96. check.Role = user.Role
  97. check.StartTime = util.GetTimeAsMsSinceEpoch(time.Now())
  98. check.conn = conn
  99. check.updateUserPermissions()
  100. c.Checks = append(c.Checks, check)
  101. return &check
  102. }
  103. // remove a user from the ones with active retention checks
  104. // and returns true if the user is removed
  105. func (c *ActiveRetentionChecks) remove(username string) bool {
  106. c.Lock()
  107. defer c.Unlock()
  108. for idx, check := range c.Checks {
  109. if check.Username == username {
  110. lastIdx := len(c.Checks) - 1
  111. c.Checks[idx] = c.Checks[lastIdx]
  112. c.Checks = c.Checks[:lastIdx]
  113. return true
  114. }
  115. }
  116. return false
  117. }
  118. type folderRetentionCheckResult struct {
  119. Path string `json:"path"`
  120. Retention int `json:"retention"`
  121. DeletedFiles int `json:"deleted_files"`
  122. DeletedSize int64 `json:"deleted_size"`
  123. Elapsed time.Duration `json:"-"`
  124. Info string `json:"info,omitempty"`
  125. Error string `json:"error,omitempty"`
  126. }
  127. // RetentionCheck defines an active retention check
  128. type RetentionCheck struct {
  129. // Username to which the retention check refers
  130. Username string `json:"username"`
  131. // retention check start time as unix timestamp in milliseconds
  132. StartTime int64 `json:"start_time"`
  133. // affected folders
  134. Folders []dataprovider.FolderRetention `json:"folders"`
  135. // how cleanup results will be notified
  136. Notifications []RetentionCheckNotification `json:"notifications,omitempty"`
  137. // email to use if the notification method is set to email
  138. Email string `json:"email,omitempty"`
  139. Role string `json:"-"`
  140. // Cleanup results
  141. results []folderRetentionCheckResult `json:"-"`
  142. conn *BaseConnection
  143. }
  144. // Validate returns an error if the specified folders are not valid
  145. func (c *RetentionCheck) Validate() error {
  146. folderPaths := make(map[string]bool)
  147. nothingToDo := true
  148. for idx := range c.Folders {
  149. f := &c.Folders[idx]
  150. if err := f.Validate(); err != nil {
  151. return err
  152. }
  153. if f.Retention > 0 {
  154. nothingToDo = false
  155. }
  156. if _, ok := folderPaths[f.Path]; ok {
  157. return util.NewValidationError(fmt.Sprintf("duplicated folder path %q", f.Path))
  158. }
  159. folderPaths[f.Path] = true
  160. }
  161. if nothingToDo {
  162. return util.NewValidationError("nothing to delete!")
  163. }
  164. for _, notification := range c.Notifications {
  165. switch notification {
  166. case RetentionCheckNotificationEmail:
  167. if !smtp.IsEnabled() {
  168. return util.NewValidationError("in order to notify results via email you must configure an SMTP server")
  169. }
  170. if c.Email == "" {
  171. return util.NewValidationError("in order to notify results via email you must add a valid email address to your profile")
  172. }
  173. case RetentionCheckNotificationHook:
  174. if Config.DataRetentionHook == "" {
  175. return util.NewValidationError("in order to notify results via hook you must define a data_retention_hook")
  176. }
  177. default:
  178. return util.NewValidationError(fmt.Sprintf("invalid notification %q", notification))
  179. }
  180. }
  181. return nil
  182. }
  183. func (c *RetentionCheck) updateUserPermissions() {
  184. for k := range c.conn.User.Permissions {
  185. c.conn.User.Permissions[k] = []string{dataprovider.PermAny}
  186. }
  187. }
  188. func (c *RetentionCheck) getFolderRetention(folderPath string) (dataprovider.FolderRetention, error) {
  189. dirsForPath := util.GetDirsForVirtualPath(folderPath)
  190. for _, dirPath := range dirsForPath {
  191. for _, folder := range c.Folders {
  192. if folder.Path == dirPath {
  193. return folder, nil
  194. }
  195. }
  196. }
  197. return dataprovider.FolderRetention{}, fmt.Errorf("unable to find folder retention for %q", folderPath)
  198. }
  199. func (c *RetentionCheck) removeFile(virtualPath string, info os.FileInfo) error {
  200. fs, fsPath, err := c.conn.GetFsAndResolvedPath(virtualPath)
  201. if err != nil {
  202. return err
  203. }
  204. return c.conn.RemoveFile(fs, fsPath, virtualPath, info)
  205. }
  206. func (c *RetentionCheck) cleanupFolder(folderPath string, recursion int) error {
  207. startTime := time.Now()
  208. result := folderRetentionCheckResult{
  209. Path: folderPath,
  210. }
  211. defer func() {
  212. c.results = append(c.results, result)
  213. }()
  214. if recursion >= util.MaxRecursion {
  215. result.Elapsed = time.Since(startTime)
  216. result.Info = "data retention check skipped: recursion too deep"
  217. c.conn.Log(logger.LevelError, "data retention check skipped, recursion too depth for %q: %d",
  218. folderPath, recursion)
  219. return util.ErrRecursionTooDeep
  220. }
  221. recursion++
  222. folderRetention, err := c.getFolderRetention(folderPath)
  223. if err != nil {
  224. result.Elapsed = time.Since(startTime)
  225. result.Error = "unable to get folder retention"
  226. c.conn.Log(logger.LevelError, "unable to get folder retention for path %q", folderPath)
  227. return err
  228. }
  229. result.Retention = folderRetention.Retention
  230. if folderRetention.Retention == 0 {
  231. result.Elapsed = time.Since(startTime)
  232. result.Info = "data retention check skipped: retention is set to 0"
  233. c.conn.Log(logger.LevelDebug, "retention check skipped for folder %q, retention is set to 0", folderPath)
  234. return nil
  235. }
  236. c.conn.Log(logger.LevelDebug, "start retention check for folder %q, retention: %v hours, delete empty dirs? %v",
  237. folderPath, folderRetention.Retention, folderRetention.DeleteEmptyDirs)
  238. lister, err := c.conn.ListDir(folderPath)
  239. if err != nil {
  240. result.Elapsed = time.Since(startTime)
  241. if err == c.conn.GetNotExistError() {
  242. result.Info = "data retention check skipped, folder does not exist"
  243. c.conn.Log(logger.LevelDebug, "folder %q does not exist, retention check skipped", folderPath)
  244. return nil
  245. }
  246. result.Error = fmt.Sprintf("unable to get lister for directory %q", folderPath)
  247. c.conn.Log(logger.LevelError, result.Error)
  248. return err
  249. }
  250. defer lister.Close()
  251. for {
  252. files, err := lister.Next(vfs.ListerBatchSize)
  253. finished := errors.Is(err, io.EOF)
  254. if err := lister.convertError(err); err != nil {
  255. result.Elapsed = time.Since(startTime)
  256. result.Error = fmt.Sprintf("unable to list directory %q", folderPath)
  257. c.conn.Log(logger.LevelError, "unable to list dir %q: %v", folderPath, err)
  258. return err
  259. }
  260. for _, info := range files {
  261. virtualPath := path.Join(folderPath, info.Name())
  262. if info.IsDir() {
  263. if err := c.cleanupFolder(virtualPath, recursion); err != nil {
  264. result.Elapsed = time.Since(startTime)
  265. result.Error = fmt.Sprintf("unable to check folder: %v", err)
  266. c.conn.Log(logger.LevelError, "unable to cleanup folder %q: %v", virtualPath, err)
  267. return err
  268. }
  269. } else {
  270. retentionTime := info.ModTime().Add(time.Duration(folderRetention.Retention) * time.Hour)
  271. if retentionTime.Before(time.Now()) {
  272. if err := c.removeFile(virtualPath, info); err != nil {
  273. result.Elapsed = time.Since(startTime)
  274. result.Error = fmt.Sprintf("unable to remove file %q: %v", virtualPath, err)
  275. c.conn.Log(logger.LevelError, "unable to remove file %q, retention %v: %v",
  276. virtualPath, retentionTime, err)
  277. return err
  278. }
  279. c.conn.Log(logger.LevelDebug, "removed file %q, modification time: %v, retention: %v hours, retention time: %v",
  280. virtualPath, info.ModTime(), folderRetention.Retention, retentionTime)
  281. result.DeletedFiles++
  282. result.DeletedSize += info.Size()
  283. }
  284. }
  285. }
  286. if finished {
  287. break
  288. }
  289. }
  290. lister.Close()
  291. c.checkEmptyDirRemoval(folderPath, folderRetention.DeleteEmptyDirs)
  292. result.Elapsed = time.Since(startTime)
  293. c.conn.Log(logger.LevelDebug, "retention check completed for folder %q, deleted files: %v, deleted size: %v bytes",
  294. folderPath, result.DeletedFiles, result.DeletedSize)
  295. return nil
  296. }
  297. func (c *RetentionCheck) checkEmptyDirRemoval(folderPath string, checkVal bool) {
  298. if folderPath == "/" || !checkVal {
  299. return
  300. }
  301. for _, folder := range c.Folders {
  302. if folderPath == folder.Path {
  303. return
  304. }
  305. }
  306. if c.conn.User.HasAnyPerm([]string{
  307. dataprovider.PermDelete,
  308. dataprovider.PermDeleteDirs,
  309. }, path.Dir(folderPath),
  310. ) {
  311. lister, err := c.conn.ListDir(folderPath)
  312. if err == nil {
  313. files, err := lister.Next(1)
  314. lister.Close()
  315. if len(files) == 0 && errors.Is(err, io.EOF) {
  316. err = c.conn.RemoveDir(folderPath)
  317. c.conn.Log(logger.LevelDebug, "tried to remove empty dir %q, error: %v", folderPath, err)
  318. }
  319. }
  320. }
  321. }
  322. // Start starts the retention check
  323. func (c *RetentionCheck) Start() error {
  324. c.conn.Log(logger.LevelInfo, "retention check started")
  325. defer RetentionChecks.remove(c.conn.User.Username)
  326. defer c.conn.CloseFS() //nolint:errcheck
  327. startTime := time.Now()
  328. for _, folder := range c.Folders {
  329. if folder.Retention > 0 {
  330. if err := c.cleanupFolder(folder.Path, 0); err != nil {
  331. c.conn.Log(logger.LevelError, "retention check failed, unable to cleanup folder %q", folder.Path)
  332. c.sendNotifications(time.Since(startTime), err)
  333. return err
  334. }
  335. }
  336. }
  337. c.conn.Log(logger.LevelInfo, "retention check completed")
  338. c.sendNotifications(time.Since(startTime), nil)
  339. return nil
  340. }
  341. func (c *RetentionCheck) sendNotifications(elapsed time.Duration, err error) {
  342. for _, notification := range c.Notifications {
  343. switch notification {
  344. case RetentionCheckNotificationEmail:
  345. c.sendEmailNotification(err) //nolint:errcheck
  346. case RetentionCheckNotificationHook:
  347. c.sendHookNotification(elapsed, err) //nolint:errcheck
  348. }
  349. }
  350. }
  351. func (c *RetentionCheck) sendEmailNotification(errCheck error) error {
  352. params := EventParams{}
  353. if len(c.results) > 0 || errCheck != nil {
  354. params.retentionChecks = append(params.retentionChecks, executedRetentionCheck{
  355. Username: c.conn.User.Username,
  356. ActionName: "Retention check",
  357. Results: c.results,
  358. })
  359. }
  360. var files []*mail.File
  361. f, err := params.getRetentionReportsAsMailAttachment()
  362. if err != nil {
  363. c.conn.Log(logger.LevelError, "unable to get retention report as mail attachment: %v", err)
  364. return err
  365. }
  366. f.Name = "retention-report.zip"
  367. files = append(files, f)
  368. startTime := time.Now()
  369. var subject string
  370. if errCheck == nil {
  371. subject = fmt.Sprintf("Successful retention check for user %q", c.conn.User.Username)
  372. } else {
  373. subject = fmt.Sprintf("Retention check failed for user %q", c.conn.User.Username)
  374. }
  375. body := "Further details attached."
  376. err = smtp.SendEmail([]string{c.Email}, nil, subject, body, smtp.EmailContentTypeTextPlain, files...)
  377. if err != nil {
  378. c.conn.Log(logger.LevelError, "unable to notify retention check result via email: %v, elapsed: %s", err,
  379. time.Since(startTime))
  380. return err
  381. }
  382. c.conn.Log(logger.LevelInfo, "retention check result successfully notified via email, elapsed: %s", time.Since(startTime))
  383. return nil
  384. }
  385. func (c *RetentionCheck) sendHookNotification(elapsed time.Duration, errCheck error) error {
  386. startNewHook()
  387. defer hookEnded()
  388. data := make(map[string]any)
  389. totalDeletedFiles := 0
  390. totalDeletedSize := int64(0)
  391. for _, result := range c.results {
  392. totalDeletedFiles += result.DeletedFiles
  393. totalDeletedSize += result.DeletedSize
  394. }
  395. data["username"] = c.conn.User.Username
  396. data["start_time"] = c.StartTime
  397. data["elapsed"] = elapsed.Milliseconds()
  398. if errCheck == nil {
  399. data["status"] = 1
  400. } else {
  401. data["status"] = 0
  402. }
  403. data["total_deleted_files"] = totalDeletedFiles
  404. data["total_deleted_size"] = totalDeletedSize
  405. data["details"] = c.results
  406. jsonData, _ := json.Marshal(data)
  407. startTime := time.Now()
  408. if strings.HasPrefix(Config.DataRetentionHook, "http") {
  409. var url *url.URL
  410. url, err := url.Parse(Config.DataRetentionHook)
  411. if err != nil {
  412. c.conn.Log(logger.LevelError, "invalid data retention hook %q: %v", Config.DataRetentionHook, err)
  413. return err
  414. }
  415. respCode := 0
  416. resp, err := httpclient.RetryablePost(url.String(), "application/json", bytes.NewBuffer(jsonData))
  417. if err == nil {
  418. respCode = resp.StatusCode
  419. resp.Body.Close()
  420. if respCode != http.StatusOK {
  421. err = errUnexpectedHTTResponse
  422. }
  423. }
  424. c.conn.Log(logger.LevelDebug, "notified result to URL: %q, status code: %v, elapsed: %v err: %v",
  425. url.Redacted(), respCode, time.Since(startTime), err)
  426. return err
  427. }
  428. if !filepath.IsAbs(Config.DataRetentionHook) {
  429. err := fmt.Errorf("invalid data retention hook %q", Config.DataRetentionHook)
  430. c.conn.Log(logger.LevelError, "%v", err)
  431. return err
  432. }
  433. timeout, env, args := command.GetConfig(Config.DataRetentionHook, command.HookDataRetention)
  434. ctx, cancel := context.WithTimeout(context.Background(), timeout)
  435. defer cancel()
  436. cmd := exec.CommandContext(ctx, Config.DataRetentionHook, args...)
  437. cmd.Env = append(env,
  438. fmt.Sprintf("SFTPGO_DATA_RETENTION_RESULT=%s", util.BytesToString(jsonData)))
  439. err := cmd.Run()
  440. c.conn.Log(logger.LevelDebug, "notified result using command: %q, elapsed: %s err: %v",
  441. Config.DataRetentionHook, time.Since(startTime), err)
  442. return err
  443. }