| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- package storage
- import (
- "context"
- "fmt"
- "io"
- "log/slog"
- "os"
- "path/filepath"
- "github.com/anacrolix/log"
- "github.com/anacrolix/missinggo/v2"
- "github.com/anacrolix/torrent/common"
- "github.com/anacrolix/torrent/metainfo"
- "github.com/anacrolix/torrent/segments"
- )
- // File-based storage for torrents, that isn't yet bound to a particular torrent.
- type fileClientImpl struct {
- opts NewFileClientOpts
- }
- // All Torrent data stored in this baseDir. The info names of each torrent are used as directories.
- func NewFile(baseDir string) ClientImplCloser {
- return NewFileWithCompletion(baseDir, pieceCompletionForDir(baseDir))
- }
- type NewFileClientOpts struct {
- // The base directory for all downloads.
- ClientBaseDir string
- FilePathMaker FilePathMaker
- TorrentDirMaker TorrentDirFilePathMaker
- PieceCompletion PieceCompletion
- }
- // NewFileOpts creates a new ClientImplCloser that stores files using the OS native filesystem.
- func NewFileOpts(opts NewFileClientOpts) ClientImplCloser {
- if opts.TorrentDirMaker == nil {
- opts.TorrentDirMaker = defaultPathMaker
- }
- if opts.FilePathMaker == nil {
- opts.FilePathMaker = func(opts FilePathMakerOpts) string {
- var parts []string
- if opts.Info.BestName() != metainfo.NoName {
- parts = append(parts, opts.Info.BestName())
- }
- return filepath.Join(append(parts, opts.File.BestPath()...)...)
- }
- }
- if opts.PieceCompletion == nil {
- opts.PieceCompletion = pieceCompletionForDir(opts.ClientBaseDir)
- }
- return fileClientImpl{opts}
- }
- func (me fileClientImpl) Close() error {
- return me.opts.PieceCompletion.Close()
- }
- func (fs fileClientImpl) OpenTorrent(
- ctx context.Context,
- info *metainfo.Info,
- infoHash metainfo.Hash,
- ) (_ TorrentImpl, err error) {
- dir := fs.opts.TorrentDirMaker(fs.opts.ClientBaseDir, info, infoHash)
- logger := log.ContextLogger(ctx).Slogger()
- logger.DebugContext(ctx, "opened file torrent storage", slog.String("dir", dir))
- upvertedFiles := info.UpvertedFiles()
- files := make([]file, 0, len(upvertedFiles))
- for i, fileInfo := range upvertedFiles {
- filePath := filepath.Join(dir, fs.opts.FilePathMaker(FilePathMakerOpts{
- Info: info,
- File: &fileInfo,
- }))
- if !isSubFilepath(dir, filePath) {
- err = fmt.Errorf("file %v: path %q is not sub path of %q", i, filePath, dir)
- return
- }
- f := file{
- path: filePath,
- length: fileInfo.Length,
- }
- if f.length == 0 {
- err = CreateNativeZeroLengthFile(f.path)
- if err != nil {
- err = fmt.Errorf("creating zero length file: %w", err)
- return
- }
- }
- files = append(files, f)
- }
- t := &fileTorrentImpl{
- files,
- segments.NewIndexFromSegments(common.TorrentOffsetFileSegments(info)),
- infoHash,
- fs.opts.PieceCompletion,
- }
- return TorrentImpl{
- Piece: t.Piece,
- Close: t.Close,
- Flush: t.Flush,
- }, nil
- }
- type file struct {
- // The safe, OS-local file path.
- path string
- length int64
- }
- type fileTorrentImpl struct {
- files []file
- segmentLocater segments.Index
- infoHash metainfo.Hash
- completion PieceCompletion
- }
- func (fts *fileTorrentImpl) Piece(p metainfo.Piece) PieceImpl {
- // Create a view onto the file-based torrent storage.
- _io := fileTorrentImplIO{fts}
- // Return the appropriate segments of this.
- return &filePieceImpl{
- fts,
- p,
- missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
- io.NewSectionReader(_io, p.Offset(), p.Length()),
- }
- }
- func (fs *fileTorrentImpl) Close() error {
- return nil
- }
- func fsync(filePath string) (err error) {
- _ = os.MkdirAll(filepath.Dir(filePath), 0o777)
- var f *os.File
- f, err = os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0o666)
- if err != nil {
- return err
- }
- defer f.Close()
- if err = f.Sync(); err != nil {
- return err
- }
- return f.Close()
- }
- func (fts *fileTorrentImpl) Flush() error {
- for _, f := range fts.files {
- if err := fsync(f.path); err != nil {
- return err
- }
- }
- return nil
- }
- // A helper to create zero-length files which won't appear for file-orientated storage since no
- // writes will ever occur to them (no torrent data is associated with a zero-length file). The
- // caller should make sure the file name provided is safe/sanitized.
- func CreateNativeZeroLengthFile(name string) error {
- os.MkdirAll(filepath.Dir(name), 0o777)
- var f io.Closer
- f, err := os.Create(name)
- if err != nil {
- return err
- }
- return f.Close()
- }
- // Exposes file-based storage of a torrent, as one big ReadWriterAt.
- type fileTorrentImplIO struct {
- fts *fileTorrentImpl
- }
- // Returns EOF on short or missing file.
- func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
- f, err := os.Open(file.path)
- if os.IsNotExist(err) {
- // File missing is treated the same as a short file.
- err = io.EOF
- return
- }
- if err != nil {
- return
- }
- defer f.Close()
- // Limit the read to within the expected bounds of this file.
- if int64(len(b)) > file.length-off {
- b = b[:file.length-off]
- }
- for off < file.length && len(b) != 0 {
- n1, err1 := f.ReadAt(b, off)
- b = b[n1:]
- n += n1
- off += int64(n1)
- if n1 == 0 {
- err = err1
- break
- }
- }
- return
- }
- // Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
- func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
- fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
- n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
- n += n1
- b = b[n1:]
- err = err1
- return err == nil // && int64(n1) == e.Length
- })
- if len(b) != 0 && err == nil {
- err = io.EOF
- }
- return
- }
- func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
- // log.Printf("write at %v: %v bytes", off, len(p))
- fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
- name := fst.fts.files[i].path
- os.MkdirAll(filepath.Dir(name), 0o777)
- var f *os.File
- f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
- if err != nil {
- return false
- }
- var n1 int
- n1, err = f.WriteAt(p[:e.Length], e.Start)
- // log.Printf("%v %v wrote %v: %v", i, e, n1, err)
- closeErr := f.Close()
- n += n1
- p = p[n1:]
- if err == nil {
- err = closeErr
- }
- if err == nil && int64(n1) != e.Length {
- err = io.ErrShortWrite
- }
- return err == nil
- })
- return
- }
|