util.go 7.8 KB

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