dataretention.go 15 KB

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