util.go 7.7 KB

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