Logger          *slog.Logger
 }
 
+// The specific part-files option or the default.
+func (me NewFileClientOpts) partFiles() bool {
+       return me.UsePartFiles.UnwrapOr(true)
+}
+
 // NewFileOpts creates a new ClientImplCloser that stores files using the OS native filesystem.
 func NewFileOpts(opts NewFileClientOpts) ClientImplCloser {
        if opts.TorrentDirMaker == nil {
                }
        }
        if opts.PieceCompletion == nil {
-               opts.PieceCompletion = pieceCompletionForDir(opts.ClientBaseDir)
+               if opts.partFiles() {
+                       opts.PieceCompletion = NewMapPieceCompletion()
+               } else {
+                       opts.PieceCompletion = pieceCompletionForDir(opts.ClientBaseDir)
+               }
        }
        if opts.Logger == nil {
                opts.Logger = log.Default.Slogger()
                infoHash,
                fs,
        }
+       if t.partFiles() {
+               err = t.setCompletionFromPartFiles()
+               if err != nil {
+                       err = fmt.Errorf("setting completion from part files: %w", err)
+                       return
+               }
+       }
        return TorrentImpl{
                Piece: t.Piece,
                Close: t.Close,
        client *fileClientImpl
 }
 
+func (fts *fileTorrentImpl) logger() *slog.Logger {
+       return fts.client.opts.Logger
+}
+
+func (fts *fileTorrentImpl) pieceCompletion() PieceCompletion {
+       return fts.client.opts.PieceCompletion
+}
+
+func (fts *fileTorrentImpl) pieceCompletionKey(p int) metainfo.PieceKey {
+       return metainfo.PieceKey{
+               InfoHash: fts.infoHash,
+               Index:    p,
+       }
+}
+
+func (fts *fileTorrentImpl) setPieceCompletion(p int, complete bool) error {
+       return fts.pieceCompletion().Set(fts.pieceCompletionKey(p), complete)
+}
+
+// Set piece completions based on whether all files in each piece are not .part files.
+func (fts *fileTorrentImpl) setCompletionFromPartFiles() error {
+       notComplete := make([]bool, fts.info.NumPieces())
+       for _, f := range fts.files {
+               fi, err := os.Stat(f.safeOsPath)
+               if err == nil {
+                       if fi.Size() == f.length {
+                               continue
+                       }
+                       fts.logger().Warn("file has unexpected size", "file", f.safeOsPath, "size", fi.Size(), "expected", f.length)
+               } else if !errors.Is(err, fs.ErrNotExist) {
+                       fts.logger().Warn("error checking file size", "err", err)
+               }
+               for i := f.beginPieceIndex; i < f.endPieceIndex; i++ {
+                       notComplete[i] = true
+               }
+       }
+       for i, nc := range notComplete {
+               if nc {
+                       // Use whatever the piece completion has, or trigger a hash.
+                       continue
+               }
+               err := fts.setPieceCompletion(i, true)
+               if err != nil {
+                       return fmt.Errorf("setting piece %v completion: %w", i, err)
+               }
+       }
+       return nil
+}
+
 func (fts *fileTorrentImpl) partFiles() bool {
-       return fts.client.opts.UsePartFiles.UnwrapOr(true)
+       return fts.client.opts.partFiles()
 }
 
 func (fts *fileTorrentImpl) pathForWrite(f file) string {
 }
 
 func (fts *fileTorrentImpl) getCompletion(piece int) Completion {
-       cmpl, err := fts.client.opts.PieceCompletion.Get(metainfo.PieceKey{
-               fts.infoHash, piece,
-       })
+       cmpl, err := fts.pieceCompletion().Get(metainfo.PieceKey{fts.infoHash, piece})
        cmpl.Err = errors.Join(cmpl.Err, err)
        return cmpl
 }