util.go 8.0 KB

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