file.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. package storage
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "log/slog"
  7. "os"
  8. "path/filepath"
  9. "github.com/anacrolix/log"
  10. "github.com/anacrolix/missinggo/v2"
  11. "github.com/anacrolix/torrent/common"
  12. "github.com/anacrolix/torrent/metainfo"
  13. "github.com/anacrolix/torrent/segments"
  14. )
  15. // File-based storage for torrents, that isn't yet bound to a particular torrent.
  16. type fileClientImpl struct {
  17. opts NewFileClientOpts
  18. }
  19. // All Torrent data stored in this baseDir. The info names of each torrent are used as directories.
  20. func NewFile(baseDir string) ClientImplCloser {
  21. return NewFileWithCompletion(baseDir, pieceCompletionForDir(baseDir))
  22. }
  23. type NewFileClientOpts struct {
  24. // The base directory for all downloads.
  25. ClientBaseDir string
  26. FilePathMaker FilePathMaker
  27. TorrentDirMaker TorrentDirFilePathMaker
  28. PieceCompletion PieceCompletion
  29. }
  30. // NewFileOpts creates a new ClientImplCloser that stores files using the OS native filesystem.
  31. func NewFileOpts(opts NewFileClientOpts) ClientImplCloser {
  32. if opts.TorrentDirMaker == nil {
  33. opts.TorrentDirMaker = defaultPathMaker
  34. }
  35. if opts.FilePathMaker == nil {
  36. opts.FilePathMaker = func(opts FilePathMakerOpts) string {
  37. var parts []string
  38. if opts.Info.BestName() != metainfo.NoName {
  39. parts = append(parts, opts.Info.BestName())
  40. }
  41. return filepath.Join(append(parts, opts.File.BestPath()...)...)
  42. }
  43. }
  44. if opts.PieceCompletion == nil {
  45. opts.PieceCompletion = pieceCompletionForDir(opts.ClientBaseDir)
  46. }
  47. return fileClientImpl{opts}
  48. }
  49. func (me fileClientImpl) Close() error {
  50. return me.opts.PieceCompletion.Close()
  51. }
  52. func (fs fileClientImpl) OpenTorrent(
  53. ctx context.Context,
  54. info *metainfo.Info,
  55. infoHash metainfo.Hash,
  56. ) (_ TorrentImpl, err error) {
  57. dir := fs.opts.TorrentDirMaker(fs.opts.ClientBaseDir, info, infoHash)
  58. logger := log.ContextLogger(ctx).Slogger()
  59. logger.DebugContext(ctx, "opened file torrent storage", slog.String("dir", dir))
  60. upvertedFiles := info.UpvertedFiles()
  61. files := make([]file, 0, len(upvertedFiles))
  62. for i, fileInfo := range upvertedFiles {
  63. filePath := filepath.Join(dir, fs.opts.FilePathMaker(FilePathMakerOpts{
  64. Info: info,
  65. File: &fileInfo,
  66. }))
  67. if !isSubFilepath(dir, filePath) {
  68. err = fmt.Errorf("file %v: path %q is not sub path of %q", i, filePath, dir)
  69. return
  70. }
  71. f := file{
  72. path: filePath,
  73. length: fileInfo.Length,
  74. }
  75. if f.length == 0 {
  76. err = CreateNativeZeroLengthFile(f.path)
  77. if err != nil {
  78. err = fmt.Errorf("creating zero length file: %w", err)
  79. return
  80. }
  81. }
  82. files = append(files, f)
  83. }
  84. t := &fileTorrentImpl{
  85. files,
  86. segments.NewIndexFromSegments(common.TorrentOffsetFileSegments(info)),
  87. infoHash,
  88. fs.opts.PieceCompletion,
  89. }
  90. return TorrentImpl{
  91. Piece: t.Piece,
  92. Close: t.Close,
  93. Flush: t.Flush,
  94. }, nil
  95. }
  96. type file struct {
  97. // The safe, OS-local file path.
  98. path string
  99. length int64
  100. }
  101. type fileTorrentImpl struct {
  102. files []file
  103. segmentLocater segments.Index
  104. infoHash metainfo.Hash
  105. completion PieceCompletion
  106. }
  107. func (fts *fileTorrentImpl) Piece(p metainfo.Piece) PieceImpl {
  108. // Create a view onto the file-based torrent storage.
  109. _io := fileTorrentImplIO{fts}
  110. // Return the appropriate segments of this.
  111. return &filePieceImpl{
  112. fts,
  113. p,
  114. missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
  115. io.NewSectionReader(_io, p.Offset(), p.Length()),
  116. }
  117. }
  118. func (fs *fileTorrentImpl) Close() error {
  119. return nil
  120. }
  121. func fsync(filePath string) (err error) {
  122. _ = os.MkdirAll(filepath.Dir(filePath), 0o777)
  123. var f *os.File
  124. f, err = os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0o666)
  125. if err != nil {
  126. return err
  127. }
  128. defer f.Close()
  129. if err = f.Sync(); err != nil {
  130. return err
  131. }
  132. return f.Close()
  133. }
  134. func (fts *fileTorrentImpl) Flush() error {
  135. for _, f := range fts.files {
  136. if err := fsync(f.path); err != nil {
  137. return err
  138. }
  139. }
  140. return nil
  141. }
  142. // A helper to create zero-length files which won't appear for file-orientated storage since no
  143. // writes will ever occur to them (no torrent data is associated with a zero-length file). The
  144. // caller should make sure the file name provided is safe/sanitized.
  145. func CreateNativeZeroLengthFile(name string) error {
  146. os.MkdirAll(filepath.Dir(name), 0o777)
  147. var f io.Closer
  148. f, err := os.Create(name)
  149. if err != nil {
  150. return err
  151. }
  152. return f.Close()
  153. }
  154. // Exposes file-based storage of a torrent, as one big ReadWriterAt.
  155. type fileTorrentImplIO struct {
  156. fts *fileTorrentImpl
  157. }
  158. // Returns EOF on short or missing file.
  159. func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
  160. f, err := os.Open(file.path)
  161. if os.IsNotExist(err) {
  162. // File missing is treated the same as a short file.
  163. err = io.EOF
  164. return
  165. }
  166. if err != nil {
  167. return
  168. }
  169. defer f.Close()
  170. // Limit the read to within the expected bounds of this file.
  171. if int64(len(b)) > file.length-off {
  172. b = b[:file.length-off]
  173. }
  174. for off < file.length && len(b) != 0 {
  175. n1, err1 := f.ReadAt(b, off)
  176. b = b[n1:]
  177. n += n1
  178. off += int64(n1)
  179. if n1 == 0 {
  180. err = err1
  181. break
  182. }
  183. }
  184. return
  185. }
  186. // Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
  187. func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
  188. fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
  189. n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
  190. n += n1
  191. b = b[n1:]
  192. err = err1
  193. return err == nil // && int64(n1) == e.Length
  194. })
  195. if len(b) != 0 && err == nil {
  196. err = io.EOF
  197. }
  198. return
  199. }
  200. func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
  201. // log.Printf("write at %v: %v bytes", off, len(p))
  202. fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
  203. name := fst.fts.files[i].path
  204. os.MkdirAll(filepath.Dir(name), 0o777)
  205. var f *os.File
  206. f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
  207. if err != nil {
  208. return false
  209. }
  210. var n1 int
  211. n1, err = f.WriteAt(p[:e.Length], e.Start)
  212. // log.Printf("%v %v wrote %v: %v", i, e, n1, err)
  213. closeErr := f.Close()
  214. n += n1
  215. p = p[n1:]
  216. if err == nil {
  217. err = closeErr
  218. }
  219. if err == nil && int64(n1) != e.Length {
  220. err = io.ErrShortWrite
  221. }
  222. return err == nil
  223. })
  224. return
  225. }