dataretention.go 12 KB

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