util.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. // Copyright (C) 2014 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. package versioner
  7. import (
  8. "fmt"
  9. "path/filepath"
  10. "regexp"
  11. "sort"
  12. "strings"
  13. "time"
  14. "github.com/pkg/errors"
  15. "github.com/syncthing/syncthing/lib/fs"
  16. "github.com/syncthing/syncthing/lib/osutil"
  17. "github.com/syncthing/syncthing/lib/util"
  18. )
  19. var errDirectory = fmt.Errorf("cannot restore on top of a directory")
  20. var errNotFound = fmt.Errorf("version not found")
  21. var errFileAlreadyExists = fmt.Errorf("file already exists")
  22. // TagFilename inserts ~tag just before the extension of the filename.
  23. func TagFilename(name, tag string) string {
  24. dir, file := filepath.Dir(name), filepath.Base(name)
  25. ext := filepath.Ext(file)
  26. withoutExt := file[:len(file)-len(ext)]
  27. return filepath.Join(dir, withoutExt+"~"+tag+ext)
  28. }
  29. var tagExp = regexp.MustCompile(`.*~([^~.]+)(?:\.[^.]+)?$`)
  30. // extractTag returns the tag from a filename, whether at the end or middle.
  31. func extractTag(path string) string {
  32. match := tagExp.FindStringSubmatch(path)
  33. // match is []string{"whole match", "submatch"} when successful
  34. if len(match) != 2 {
  35. return ""
  36. }
  37. return match[1]
  38. }
  39. // UntagFilename returns the filename without tag, and the extracted tag
  40. func UntagFilename(path string) (string, string) {
  41. ext := filepath.Ext(path)
  42. versionTag := extractTag(path)
  43. // Files tagged with old style tags cannot be untagged.
  44. if versionTag == "" {
  45. return "", ""
  46. }
  47. // Old style tag
  48. if strings.HasSuffix(ext, versionTag) {
  49. return strings.TrimSuffix(path, "~"+versionTag), versionTag
  50. }
  51. withoutExt := path[:len(path)-len(ext)-len(versionTag)-1]
  52. name := withoutExt + ext
  53. return name, versionTag
  54. }
  55. func retrieveVersions(fileSystem fs.Filesystem) (map[string][]FileVersion, error) {
  56. files := make(map[string][]FileVersion)
  57. err := fileSystem.Walk(".", func(path string, f fs.FileInfo, err error) error {
  58. // Skip root (which is ok to be a symlink)
  59. if path == "." {
  60. return nil
  61. }
  62. // Skip walking if we cannot walk...
  63. if err != nil {
  64. return err
  65. }
  66. // Ignore symlinks
  67. if f.IsSymlink() {
  68. return fs.SkipDir
  69. }
  70. // No records for directories
  71. if f.IsDir() {
  72. return nil
  73. }
  74. modTime := f.ModTime().Truncate(time.Second)
  75. path = osutil.NormalizedFilename(path)
  76. name, tag := UntagFilename(path)
  77. // Something invalid, assume it's an untagged file (trashcan versioner stuff)
  78. if name == "" || tag == "" {
  79. files[path] = append(files[path], FileVersion{
  80. VersionTime: modTime,
  81. ModTime: modTime,
  82. Size: f.Size(),
  83. })
  84. return nil
  85. }
  86. versionTime, err := time.ParseInLocation(TimeFormat, tag, time.Local)
  87. if err != nil {
  88. // Can't parse it, welp, continue
  89. return nil
  90. }
  91. files[name] = append(files[name], FileVersion{
  92. VersionTime: versionTime,
  93. ModTime: modTime,
  94. Size: f.Size(),
  95. })
  96. return nil
  97. })
  98. if err != nil {
  99. return nil, err
  100. }
  101. return files, nil
  102. }
  103. type fileTagger func(string, string) string
  104. func archiveFile(srcFs, dstFs fs.Filesystem, filePath string, tagger fileTagger) error {
  105. filePath = osutil.NativeFilename(filePath)
  106. info, err := srcFs.Lstat(filePath)
  107. if fs.IsNotExist(err) {
  108. l.Debugln("not archiving nonexistent file", filePath)
  109. return nil
  110. } else if err != nil {
  111. return err
  112. }
  113. if info.IsSymlink() {
  114. panic("bug: attempting to version a symlink")
  115. }
  116. _, err = dstFs.Stat(".")
  117. if err != nil {
  118. if fs.IsNotExist(err) {
  119. l.Debugln("creating versions dir")
  120. err := dstFs.Mkdir(".", 0755)
  121. if err != nil {
  122. return err
  123. }
  124. _ = dstFs.Hide(".")
  125. } else {
  126. return err
  127. }
  128. }
  129. file := filepath.Base(filePath)
  130. inFolderPath := filepath.Dir(filePath)
  131. err = dstFs.MkdirAll(inFolderPath, 0755)
  132. if err != nil && !fs.IsExist(err) {
  133. l.Debugln("archiving", filePath, err)
  134. return err
  135. }
  136. now := time.Now()
  137. ver := tagger(file, now.Format(TimeFormat))
  138. dst := filepath.Join(inFolderPath, ver)
  139. l.Debugln("archiving", filePath, "moving to", dst)
  140. err = osutil.RenameOrCopy(srcFs, dstFs, filePath, dst)
  141. mtime := info.ModTime()
  142. // If it's a trashcan versioner type thing, then it does not have version time in the name
  143. // so use mtime for that.
  144. if ver == file {
  145. mtime = now
  146. }
  147. _ = dstFs.Chtimes(dst, mtime, mtime)
  148. return err
  149. }
  150. func restoreFile(src, dst fs.Filesystem, filePath string, versionTime time.Time, tagger fileTagger) error {
  151. tag := versionTime.In(time.Local).Truncate(time.Second).Format(TimeFormat)
  152. taggedFilePath := tagger(filePath, tag)
  153. // If the something already exists where we are restoring to, archive existing file for versioning
  154. // remove if it's a symlink, or fail if it's a directory
  155. if info, err := dst.Lstat(filePath); err == nil {
  156. switch {
  157. case info.IsDir():
  158. return errDirectory
  159. case info.IsSymlink():
  160. // Remove existing symlinks (as we don't want to archive them)
  161. if err := dst.Remove(filePath); err != nil {
  162. return errors.Wrap(err, "removing existing symlink")
  163. }
  164. case info.IsRegular():
  165. if err := archiveFile(dst, src, filePath, tagger); err != nil {
  166. return errors.Wrap(err, "archiving existing file")
  167. }
  168. default:
  169. panic("bug: unknown item type")
  170. }
  171. } else if !fs.IsNotExist(err) {
  172. return err
  173. }
  174. filePath = osutil.NativeFilename(filePath)
  175. // Try and find a file that has the correct mtime
  176. sourceFile := ""
  177. sourceMtime := time.Time{}
  178. if info, err := src.Lstat(taggedFilePath); err == nil && info.IsRegular() {
  179. sourceFile = taggedFilePath
  180. sourceMtime = info.ModTime()
  181. } else if err == nil {
  182. l.Debugln("restore:", taggedFilePath, "not regular")
  183. } else {
  184. l.Debugln("restore:", taggedFilePath, err.Error())
  185. }
  186. // Check for untagged file
  187. if sourceFile == "" {
  188. info, err := src.Lstat(filePath)
  189. if err == nil && info.IsRegular() && info.ModTime().Truncate(time.Second).Equal(versionTime) {
  190. sourceFile = filePath
  191. sourceMtime = info.ModTime()
  192. }
  193. }
  194. if sourceFile == "" {
  195. return errNotFound
  196. }
  197. // Check that the target location of where we are supposed to restore does not exist.
  198. // This should have been taken care of by the first few lines of this function.
  199. if _, err := dst.Lstat(filePath); err == nil {
  200. return errFileAlreadyExists
  201. } else if !fs.IsNotExist(err) {
  202. return err
  203. }
  204. _ = dst.MkdirAll(filepath.Dir(filePath), 0755)
  205. err := osutil.RenameOrCopy(src, dst, sourceFile, filePath)
  206. _ = dst.Chtimes(filePath, sourceMtime, sourceMtime)
  207. return err
  208. }
  209. func fsFromParams(folderFs fs.Filesystem, params map[string]string) (versionsFs fs.Filesystem) {
  210. if params["fsType"] == "" && params["fsPath"] == "" {
  211. versionsFs = fs.NewFilesystem(folderFs.Type(), filepath.Join(folderFs.URI(), ".stversions"))
  212. } else if params["fsType"] == "" {
  213. uri := params["fsPath"]
  214. // We only know how to deal with relative folders for basic filesystems, as that's the only one we know
  215. // how to check if it's absolute or relative.
  216. if folderFs.Type() == fs.FilesystemTypeBasic && !filepath.IsAbs(params["fsPath"]) {
  217. uri = filepath.Join(folderFs.URI(), params["fsPath"])
  218. }
  219. versionsFs = fs.NewFilesystem(folderFs.Type(), uri)
  220. } else {
  221. var fsType fs.FilesystemType
  222. _ = fsType.UnmarshalText([]byte(params["fsType"]))
  223. versionsFs = fs.NewFilesystem(fsType, params["fsPath"])
  224. }
  225. l.Debugf("%s (%s) folder using %s (%s) versioner dir", folderFs.URI(), folderFs.Type(), versionsFs.URI(), versionsFs.Type())
  226. return
  227. }
  228. func findAllVersions(fs fs.Filesystem, filePath string) []string {
  229. inFolderPath := filepath.Dir(filePath)
  230. file := filepath.Base(filePath)
  231. // Glob according to the new file~timestamp.ext pattern.
  232. pattern := filepath.Join(inFolderPath, TagFilename(file, timeGlob))
  233. versions, err := fs.Glob(pattern)
  234. if err != nil {
  235. l.Warnln("globbing:", err, "for", pattern)
  236. return nil
  237. }
  238. versions = util.UniqueTrimmedStrings(versions)
  239. sort.Strings(versions)
  240. return versions
  241. }