dataretention.go 15 KB

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