Skip to content

Commit 206e5c7

Browse files
0g-peterzhbclaude
andcommitted
transfer/indexer: say which file blocks a download and what to do (#200)
checkFileExistence reported both outcomes without saying which file it meant, so what reached the user was "Failed to download file: Failed to check file existence: File already exists" with no path at all. For the caller's own destination that is merely terse. For a fragment temp file it misleads: fragments are downloaded to <root>.temp in the process working directory, and if one survives an attempt - the copy into the output failed, os.Remove failed, the process was killed mid-iteration - every later attempt reports "File already exists" for a file the caller never created. They inspect the output file they asked for, find nothing wrong, delete it, retry, and get the same error, with nothing indicating what is actually blocking them. Name the file in both errors, and at the fragment sites add what to do about it via a single shared FragmentLeftBehindError rather than six copies of the sentence. The hint does not repeat the path, since checkFileExistence now supplies it. It is deliberately fragment-specific: the same ErrFileAlreadyExists is reported for the caller's own destination, where "remove it" would be bad advice since they may want to keep the file they already have. Exported because indexer.Client runs the same fragment loops; the alternative was two unexported copies. ErrFileAlreadyExists stays wrapped rather than replaced, so the errors.Is checks that depend on it keep matching - download_dir relies on it in three places. Reusing the leftover instead was tried in #199 and dropped: checkFileExistence compares only the merkle root, never the size, and because the root covers zero-padded chunks a file short by under a chunk of trailing zeros has the same root - 16 bytes and 256 bytes of the same padded content produce identical roots. Telling the caller to remove it needs no such trust. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent dfec67f commit 206e5c7

5 files changed

Lines changed: 112 additions & 2 deletions

File tree

indexer/client.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,11 @@ func (c *Client) downloadPlainFragments(ctx context.Context, roots []string, fil
414414
return err
415415
}
416416
err = downloader.Download(ctx, root, tempFile, withProof)
417+
if errors.Is(err, transfer.ErrFileAlreadyExists) {
418+
// A complete fragment file from an earlier attempt blocks this one. Say so and
419+
// name it: the caller never created it and has no way to guess it is the obstacle.
420+
return transfer.FragmentLeftBehindError(err)
421+
}
417422
if err != nil {
418423
return errors.WithMessage(err, "Failed to download file")
419424
}
@@ -460,6 +465,9 @@ func (c *Client) downloadEncryptedFragments(ctx context.Context, roots []string,
460465
}
461466
err = downloader.Download(ctx, root, tempFile, withProof)
462467
if err != nil {
468+
if errors.Is(err, transfer.ErrFileAlreadyExists) {
469+
return transfer.FragmentLeftBehindError(err)
470+
}
463471
return errors.WithMessage(err, fmt.Sprintf("Failed to download fragment %d", i))
464472
}
465473

transfer/downloader.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,21 @@ var createOutputFile = func(name string) (io.WriteCloser, error) {
6262
return file, nil
6363
}
6464

65+
// FragmentLeftBehindError reports that a fragment temp file from an earlier attempt is
66+
// blocking this download, and says what to do about it.
67+
//
68+
// The advice is specific to fragment temp files. checkFileExistence reports the same
69+
// ErrFileAlreadyExists for the caller's own destination, where "remove it" would be bad
70+
// advice - they may well want to keep the file they already have. It is exported because
71+
// indexer.Client runs the same fragment loops.
72+
//
73+
// err is wrapped rather than replaced, so it keeps carrying both ErrFileAlreadyExists
74+
// for errors.Is and the path that checkFileExistence added; the message deliberately
75+
// does not repeat that path.
76+
func FragmentLeftBehindError(err error) error {
77+
return errors.WithMessage(err, "a previous attempt left this fragment file behind; remove it and retry")
78+
}
79+
6580
type IDownloader interface {
6681
Download(ctx context.Context, root, filename string, withProof bool) error
6782
DownloadFragments(ctx context.Context, roots []string, filename string, withProof bool) error
@@ -132,6 +147,11 @@ func (downloader *Downloader) downloadPlainFragments(ctx context.Context, roots
132147
for _, root := range roots {
133148
tempFile := fmt.Sprintf("%v.temp", root)
134149
err := downloader.Download(ctx, root, tempFile, withProof)
150+
if errors.Is(err, ErrFileAlreadyExists) {
151+
// A complete fragment file from an earlier attempt blocks this one. Say so and
152+
// name it: the caller never created it and has no way to guess it is the obstacle.
153+
return FragmentLeftBehindError(err)
154+
}
135155
if err != nil {
136156
return errors.WithMessage(err, "Failed to download file")
137157
}
@@ -173,6 +193,9 @@ func (downloader *Downloader) downloadEncryptedFragments(ctx context.Context, ro
173193

174194
// Download raw (without decryption)
175195
if err := downloader.downloadAndValidate(ctx, root, tempFile, withProof); err != nil {
196+
if errors.Is(err, ErrFileAlreadyExists) {
197+
return FragmentLeftBehindError(err)
198+
}
176199
return errors.WithMessage(err, fmt.Sprintf("Failed to download fragment %d", i))
177200
}
178201

@@ -288,11 +311,16 @@ func checkFileExistence(filename string, hash common.Hash) error {
288311
return errors.WithMessage(err, "Failed to create file merkle tree")
289312
}
290313

314+
// Name the file. Both of these are reported for a fragment's temp file as well as for
315+
// the caller's own destination, and without the path the message sends the reader to
316+
// the wrong place: they see "File already exists", inspect the output file they asked
317+
// for, find nothing wrong with it, and never learn the obstacle is a <root>.temp they
318+
// did not create.
291319
if tree.Root() == hash {
292-
return ErrFileAlreadyExists
320+
return errors.WithMessagef(ErrFileAlreadyExists, "%v", filename)
293321
}
294322

295-
return errors.New("File already exists with different hash")
323+
return errors.Errorf("File already exists with different hash: %v", filename)
296324
}
297325

298326
func (downloader *Downloader) downloadFile(ctx context.Context, filename string, root common.Hash, info *node.FileInfo, withProof bool) error {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package transfer
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/0gfoundation/0g-storage-client/core"
9+
"github.com/ethereum/go-ethereum/common"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
// Both existence errors are reported for a fragment's temp file as well as for the
15+
// caller's own destination. Without the path, someone hitting the fragment case reads
16+
// "File already exists", inspects the output file they asked for, finds nothing wrong
17+
// with it, and never learns the obstacle is a <root>.temp they never created.
18+
func TestCheckFileExistence_NamesTheFile(t *testing.T) {
19+
path := filepath.Join(t.TempDir(), "0x1111.temp")
20+
require.NoError(t, os.WriteFile(path, []byte("a completed fragment"), 0644))
21+
22+
root, err := core.MerkleRoot(path)
23+
require.NoError(t, err)
24+
25+
matching := checkFileExistence(path, root)
26+
require.Error(t, matching)
27+
assert.ErrorIs(t, matching, ErrFileAlreadyExists,
28+
"download_dir matches on this sentinel, so it must stay wrapped rather than replaced")
29+
assert.Contains(t, matching.Error(), path, "the message must say which file")
30+
31+
mismatched := checkFileExistence(path, common.HexToHash("0xdead"))
32+
require.Error(t, mismatched)
33+
assert.NotErrorIs(t, mismatched, ErrFileAlreadyExists, "a different hash is a distinct, fatal error")
34+
assert.Contains(t, mismatched.Error(), "different hash")
35+
assert.Contains(t, mismatched.Error(), path, "the message must say which file")
36+
}
37+
38+
// A path that does not exist is not an error at all.
39+
func TestCheckFileExistence_MissingFileIsNotAnError(t *testing.T) {
40+
assert.NoError(t, checkFileExistence(filepath.Join(t.TempDir(), "absent.dat"), common.HexToHash("0xdead")))
41+
}

transfer/hot_downloader.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ func (d *HotDownloader) downloadPlainFragments(ctx context.Context, roots []stri
166166
d.logger.WithField("fragment", i).Info("Fragment not in hot storage, falling back")
167167
}
168168
if err := d.fallback.Download(ctx, root, tempFile, withProof); err != nil {
169+
if errors.Is(err, ErrFileAlreadyExists) {
170+
return FragmentLeftBehindError(err)
171+
}
169172
return errors.WithMessage(err, fmt.Sprintf("failed to download fragment %d", i))
170173
}
171174
}
@@ -241,6 +244,9 @@ func (d *HotDownloader) downloadFragmentData(ctx context.Context, root string, i
241244
}
242245
tempFile := fmt.Sprintf("%v.temp", root)
243246
if err := d.fallback.Download(ctx, root, tempFile, withProof); err != nil {
247+
if errors.Is(err, ErrFileAlreadyExists) {
248+
return nil, FragmentLeftBehindError(err)
249+
}
244250
return nil, errors.WithMessage(err, fmt.Sprintf("failed to download fragment %d", index))
245251
}
246252
fragmentData, err := os.ReadFile(tempFile)

transfer/hot_downloader_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,3 +752,30 @@ func mustNotBeContacted(t *testing.T, what string) *httptest.Server {
752752

753753
return server
754754
}
755+
756+
// A complete fragment file from an earlier attempt blocks the next one. The caller
757+
// never created it and cannot guess it is the obstacle, so the failure has to name it
758+
// and say what to do about it.
759+
func TestHotDownloader_DownloadFragments_LeftoverFragmentTellsUserToRemoveIt(t *testing.T) {
760+
chdirTemp(t)
761+
762+
router := newTestRouter(t, "") // 404 = cache miss, so the fallback is consulted
763+
defer router.Close()
764+
765+
downloader := NewHotDownloader(
766+
node.NewHotRouterClient(router.URL, testChainID),
767+
testKey(t),
768+
&mockFallbackDownloader{downloadFunc: func(_ context.Context, _, filename string, _ bool) error {
769+
// Wrapped as checkFileExistence reports it once the file is named.
770+
return errors.WithMessagef(ErrFileAlreadyExists, "%v", filename)
771+
}},
772+
)
773+
774+
err := downloader.DownloadFragments(context.Background(), []string{"0x1111"}, "output.dat", false)
775+
776+
require.Error(t, err)
777+
assert.Contains(t, err.Error(), "0x1111.temp", "the message must name the leftover fragment file")
778+
assert.Contains(t, err.Error(), "remove it and retry", "the message must say what to do")
779+
assert.Equal(t, 1, strings.Count(err.Error(), "0x1111.temp"),
780+
"the path comes from checkFileExistence, so the hint must not repeat it")
781+
}

0 commit comments

Comments
 (0)