dataretention.go 15 KB

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