wastebasket_nix.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. //go:build !windows && !darwin && !android && !ios && !js
  2. package wastebasket
  3. import (
  4. "fmt"
  5. "io/fs"
  6. "os"
  7. "os/user"
  8. "path/filepath"
  9. "strings"
  10. "sync"
  11. "time"
  12. "github.com/Bios-Marcel/wastebasket/internal"
  13. )
  14. var (
  15. // cachedInformation makes sure we don't constantly check for the
  16. // directory and which drive it is on again.
  17. cachedInformation = &cache{
  18. init: &sync.Once{},
  19. }
  20. )
  21. type cache struct {
  22. // path is the path to the trash dir, for example
  23. // /home/marcel/.local/share/Trash.
  24. path string
  25. // topdir is the closes mountpoint of `path`.
  26. topdir string
  27. init *sync.Once
  28. err error
  29. }
  30. func getCache() (*cache, error) {
  31. cachedInformation.init.Do(func() {
  32. var homeTrashDir string
  33. // On some big distros, such as Ubuntu for example, this variable isn't
  34. // set. Instead, we will fallback to what Ubuntu does for now.
  35. if dataHome := os.Getenv("XDG_DATA_HOME"); dataHome == "" {
  36. homeDir, err := os.UserHomeDir()
  37. if err != nil {
  38. cachedInformation.err = err
  39. return
  40. }
  41. homeTrashDir = filepath.Join(homeDir, ".local", "share", "Trash")
  42. } else {
  43. homeTrashDir = filepath.Join(dataHome, "Trash")
  44. }
  45. cachedInformation.path = homeTrashDir
  46. mounts, err := internal.Mounts()
  47. if err != nil {
  48. cachedInformation.err = err
  49. } else {
  50. homeTopdir, err := topdir(mounts, cachedInformation.path)
  51. if err != nil {
  52. cachedInformation.err = err
  53. } else {
  54. cachedInformation.err = nil
  55. cachedInformation.topdir = homeTopdir
  56. }
  57. }
  58. })
  59. return cachedInformation, cachedInformation.err
  60. }
  61. func topdir(potentialTopdirs []string, path string) (string, error) {
  62. var matchingDir string
  63. for _, dir := range potentialTopdirs {
  64. // Technically mounts can be nested, so we can have more than one
  65. // match, but want the deepest possible match.
  66. if strings.HasPrefix(path, dir) && len(dir) > len(matchingDir) {
  67. matchingDir = dir
  68. }
  69. }
  70. return matchingDir, nil
  71. }
  72. func Trash(paths ...string) error {
  73. // RFC3339 defined in the time package contains the timezone offset, which
  74. // isn't defined by the spec and causes issues in some trash tools, such
  75. // as trash-cli.
  76. deletionDate := time.Now().Format("2006-01-02T15:04:05")
  77. cache, err := getCache()
  78. if err != nil {
  79. return fmt.Errorf("error determining user trash directory: %w", err)
  80. }
  81. mounts, err := internal.Mounts()
  82. if err != nil {
  83. return err
  84. }
  85. for _, absPath := range paths {
  86. var err error
  87. absPath, err = filepath.Abs(absPath)
  88. if err != nil {
  89. return err
  90. }
  91. pathTopdir, err := topdir(mounts, absPath)
  92. if err != nil {
  93. return err
  94. }
  95. // We only support absolute filenames in the home trash. For
  96. // topdirs, we use relative paths. This allows us to move a
  97. // mount, while still keeping trash files recoverable.
  98. var pathForTrashInfo string
  99. var trashDir, filesDir, infoDir string
  100. // Deleting accross partitions / mounts
  101. if cache.topdir != pathTopdir {
  102. // While getTopDir won't return an empty string with its current
  103. // impl, this can change in the future, so beteter be safe than
  104. // sorry.
  105. if pathTopdir != "" {
  106. var uid string
  107. if currentUser, err := user.Current(); err != nil {
  108. return err
  109. } else {
  110. uid = currentUser.Uid
  111. }
  112. trashDir = filepath.Join(pathTopdir, ".Trash")
  113. var useFallbackTopdirTrash bool
  114. if trashDirStat, err := os.Stat(trashDir); err != nil {
  115. if !os.IsNotExist(err) {
  116. return err
  117. }
  118. useFallbackTopdirTrash = true
  119. } else {
  120. if trashDirStat.Mode()&fs.ModeSticky != 0 {
  121. // If the topdir trash directory contains a trash for all
  122. // users, it needs to have the sticky bit set. This is only
  123. // required for .Trash though, not for .Trash-$uid.
  124. useFallbackTopdirTrash = true
  125. } else if trashDirStat.Mode()&os.ModeSymlink != 0 {
  126. // Symlinks must not be used as per spec.
  127. useFallbackTopdirTrash = true
  128. }
  129. }
  130. pathForTrashInfo, err = filepath.Rel(pathTopdir, absPath)
  131. if err != nil {
  132. return err
  133. }
  134. if !useFallbackTopdirTrash {
  135. filesDir = filepath.Join(trashDir, uid, "files")
  136. infoDir = filepath.Join(trashDir, uid, "info")
  137. } else {
  138. // If .Trash doesn't exist, we need to check for .Trash-$uid
  139. // and create it if it doesn't exist. The spec however
  140. // doesn't indicate that we should do the same with .Trash.
  141. trashDir = filepath.Join(pathTopdir, ".Trash-"+uid)
  142. filesDir = filepath.Join(trashDir, "files")
  143. infoDir = filepath.Join(trashDir, "info")
  144. }
  145. }
  146. }
  147. if trashDir == "" {
  148. // Fallback to home trash.
  149. trashDir = cache.path
  150. filesDir = filepath.Join(trashDir, "files")
  151. infoDir = filepath.Join(trashDir, "info")
  152. // Hometrash supports both relative and absolute paths.
  153. if trashParent := filepath.Dir(trashDir); strings.HasPrefix(absPath, trashParent) {
  154. relPath, err := filepath.Rel(trashParent, absPath)
  155. if err != nil {
  156. return err
  157. }
  158. pathForTrashInfo = relPath
  159. } else {
  160. pathForTrashInfo = absPath
  161. }
  162. }
  163. if err := os.MkdirAll(filesDir, 0700); err != nil && !os.IsExist(err) {
  164. return fmt.Errorf("error creating directory '%s': %w", filesDir, err)
  165. }
  166. if err := os.MkdirAll(infoDir, 0700); err != nil && !os.IsExist(err) {
  167. return fmt.Errorf("error creating directory '%s': %w", infoDir, err)
  168. }
  169. baseName := filepath.Base(absPath)
  170. trashedFilePath := filepath.Join(filesDir, baseName)
  171. trashedFileInfoPath := filepath.Join(infoDir, baseName) + ".trashinfo"
  172. // We need to check whether the trash already contains a file with this
  173. // name, since deleted files from different directories often have the
  174. // same name. An example would be .gitignore files, they always have
  175. // the same basename and therefore always the same trash path.
  176. // We simply count up in this case. Since we've got the info file, we
  177. // can map back to the original name later on.
  178. var infoFileHandle *os.File
  179. if exists, err := internal.FileExists(trashedFilePath); err != nil {
  180. return err
  181. } else if !exists {
  182. // We save ourselves the FileExists check, as we can combine it
  183. // with the opening of the file handle. This is a performance
  184. // optimisation.
  185. infoFileHandle, err = os.OpenFile(trashedFileInfoPath, os.O_EXCL|os.O_CREATE|os.O_WRONLY, 0600)
  186. if err != nil {
  187. if !os.IsExist(err) {
  188. return err
  189. }
  190. }
  191. // While we close manually later, we want to prevent a leak.
  192. defer infoFileHandle.Close()
  193. }
  194. // If there isn't a valid info file handle yet, it means that one
  195. // of the two file names were already in use, requiring us to find
  196. // two unique filenames eiter way.
  197. if infoFileHandle == nil {
  198. extension := filepath.Ext(baseName)
  199. baseNameNoExtension := strings.TrimSuffix(baseName, extension)
  200. for i := uint64(1); i != 0; i = i + 1 {
  201. newBaseName := fmt.Sprintf("%s.%d%s", baseNameNoExtension, i, extension)
  202. // The names of both files must always be the same, putting
  203. // aside the .trashinfo extension.
  204. trashedFilePath = filepath.Join(filesDir, newBaseName)
  205. if exists, err := internal.FileExists(trashedFilePath); err != nil || exists {
  206. continue
  207. }
  208. infoFileHandle, err = os.OpenFile(filepath.Join(infoDir, newBaseName+".trashinfo"), os.O_EXCL|os.O_CREATE|os.O_WRONLY, 0600)
  209. if err != nil {
  210. if os.IsExist(err) {
  211. continue
  212. }
  213. return err
  214. }
  215. defer infoFileHandle.Close()
  216. // We found a valid name, where neither the file itself, nor
  217. // the trashinfo file exist.
  218. break
  219. }
  220. }
  221. if err := os.Rename(absPath, trashedFilePath); err != nil {
  222. // We save ourselvse the exists check at the start of the loop, as
  223. // deleting non existing files probably does not happen that often.
  224. if os.IsNotExist(err) {
  225. // Since we already create the info file, we will have to manually delete it again.
  226. name := infoFileHandle.Name()
  227. infoFileHandle.Close()
  228. // We ignore the error here, it isn't super important
  229. os.Remove(name)
  230. continue
  231. }
  232. // All special treatment failed, return original os.Rename error
  233. return err
  234. }
  235. if _, err = infoFileHandle.WriteString(fmt.Sprintf("[Trash Info]\nPath=%s\nDeletionDate=%s\n", internal.EscapeUrl(pathForTrashInfo), deletionDate)); err != nil {
  236. return err
  237. }
  238. }
  239. return nil
  240. }
  241. func Empty() error {
  242. cache, err := getCache()
  243. if err != nil {
  244. return err
  245. }
  246. if err := internal.RemoveAllIfExists(cache.path); err != nil {
  247. return err
  248. }
  249. mounts, err := internal.Mounts()
  250. if err != nil {
  251. return err
  252. }
  253. currentUser, err := user.Current()
  254. if err != nil {
  255. return err
  256. }
  257. uid := currentUser.Uid
  258. for _, mount := range mounts {
  259. if err := internal.RemoveAllIfExists(filepath.Join(mount, ".Trash", uid)); err != nil && !os.IsPermission(err) {
  260. return err
  261. }
  262. if err := internal.RemoveAllIfExists(filepath.Join(mount, fmt.Sprintf(".Trash-%s", uid))); err != nil && !os.IsPermission(err) {
  263. return err
  264. }
  265. }
  266. return nil
  267. }