fix: create OpenList target directories before verification
This commit is contained in:
@@ -214,7 +214,11 @@ public sealed class OpenListClient : IOpenListClient
|
|||||||
// Cloud drivers can complete a server-side copy without invalidating
|
// Cloud drivers can complete a server-side copy without invalidating
|
||||||
// OpenList's directory cache. Refreshing the parent also makes files
|
// OpenList's directory cache. Refreshing the parent also makes files
|
||||||
// written directly into a local mount visible before they are copied.
|
// written directly into a local mount visible before they are copied.
|
||||||
await RefreshDirectoryAsync(connection, GetDirectoryName(path), cancellationToken);
|
if (!await TryRefreshDirectoryAsync(connection, GetDirectoryName(path), cancellationToken))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return await TryGetObjectCoreAsync(connection, path, cancellationToken);
|
return await TryGetObjectCoreAsync(connection, path, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +271,7 @@ public sealed class OpenListClient : IOpenListClient
|
|||||||
return new OpenListObjectInfo(name, size, isDirectory, hashes);
|
return new OpenListObjectInfo(name, size, isDirectory, hashes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RefreshDirectoryAsync(
|
private async Task<bool> TryRefreshDirectoryAsync(
|
||||||
OpenListConnectionRequest connection,
|
OpenListConnectionRequest connection,
|
||||||
string path,
|
string path,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -279,7 +283,22 @@ public sealed class OpenListClient : IOpenListClient
|
|||||||
"/api/fs/list",
|
"/api/fs/list",
|
||||||
new { path, password = string.Empty, refresh = true, page = 1, per_page = 0 }),
|
new { path, password = string.Empty, refresh = true, page = 1, per_page = 0 }),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
|
if (envelope.Code == 200)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenList commonly reports a missing directory as code 500 instead
|
||||||
|
// of 404. A caller probing a not-yet-created target should still see
|
||||||
|
// that as a normal cache miss.
|
||||||
|
if (envelope.Code == 404 || ContainsAny(envelope.Message, "not found", "object not found", "no such file"))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
EnsureSuccess(envelope, $"OpenList refresh '{path}'");
|
EnsureSuccess(envelope, $"OpenList refresh '{path}'");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OpenListCopyResult> CopyFileAsync(
|
public async Task<OpenListCopyResult> CopyFileAsync(
|
||||||
|
|||||||
@@ -375,6 +375,11 @@ public sealed class OpenListUploadQueueService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenList returns code 500 when refreshing a directory that does not
|
||||||
|
// exist yet. Create the complete streamer/date hierarchy before the
|
||||||
|
// first target probe so a new recording path can be uploaded normally.
|
||||||
|
await _openListClient.EnsureDirectoryAsync(connection, GetDirectoryName(targetPath), cancellationToken);
|
||||||
|
|
||||||
var existingTarget = await VerifyTargetAsync(connection, targetPath, localPath, expectedSize, cancellationToken);
|
var existingTarget = await VerifyTargetAsync(connection, targetPath, localPath, expectedSize, cancellationToken);
|
||||||
if (existingTarget == TargetVerification.Match)
|
if (existingTarget == TargetVerification.Match)
|
||||||
{
|
{
|
||||||
@@ -402,7 +407,6 @@ public sealed class OpenListUploadQueueService
|
|||||||
throw new InvalidOperationException($"OpenList 源文件大小不一致:期望 {expectedSize},实际 {sourceObject.Size},路径 {sourcePath}");
|
throw new InvalidOperationException($"OpenList 源文件大小不一致:期望 {expectedSize},实际 {sourceObject.Size},路径 {sourcePath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _openListClient.EnsureDirectoryAsync(connection, GetDirectoryName(targetPath), cancellationToken);
|
|
||||||
var copyResult = await _openListClient.CopyFileAsync(connection, sourcePath, targetPath, cancellationToken);
|
var copyResult = await _openListClient.CopyFileAsync(connection, sourcePath, targetPath, cancellationToken);
|
||||||
if (copyResult.TaskIds.Count == 0)
|
if (copyResult.TaskIds.Count == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -123,6 +123,38 @@ public sealed class OpenListUploadTests
|
|||||||
Assert.Equal("/archive/主播", refreshedPath);
|
Assert.Equal("/archive/主播", refreshedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryGetObject_TreatsMissingParentDuringRefreshAsCacheMiss()
|
||||||
|
{
|
||||||
|
var getCount = 0;
|
||||||
|
var refreshedPath = string.Empty;
|
||||||
|
var handler = new StubHttpMessageHandler(async request =>
|
||||||
|
{
|
||||||
|
if (request.RequestUri!.AbsolutePath.EndsWith("/api/auth/login", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return JsonResponse("""{"code":200,"message":"success","data":{"token":"test-token"}}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = JsonDocument.Parse(await request.Content!.ReadAsStringAsync()).RootElement;
|
||||||
|
if (request.RequestUri.AbsolutePath.EndsWith("/api/fs/list", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
refreshedPath = body.GetProperty("path").GetString();
|
||||||
|
return JsonResponse("""{"code":500,"message":"failed get objs: failed get dir: object not found","data":null}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.EndsWith("/api/fs/get", request.RequestUri.AbsolutePath, StringComparison.Ordinal);
|
||||||
|
getCount++;
|
||||||
|
return JsonResponse("""{"code":500,"message":"object not found","data":null}""");
|
||||||
|
});
|
||||||
|
var client = CreateClient(handler);
|
||||||
|
|
||||||
|
var result = await client.TryGetObjectAsync(Connection(), "/archive/新主播/2026-08-01/segment.mp4");
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
Assert.Equal(1, getCount);
|
||||||
|
Assert.Equal("/archive/新主播/2026-08-01", refreshedPath);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CopyAndTaskPolling_UseOpenListV423Contract()
|
public async Task CopyAndTaskPolling_UseOpenListV423Contract()
|
||||||
{
|
{
|
||||||
@@ -313,6 +345,10 @@ public sealed class OpenListUploadTests
|
|||||||
Assert.Equal(twoGiB, job.VideoSizeBytes);
|
Assert.Equal(twoGiB, job.VideoSizeBytes);
|
||||||
Assert.True(await fixture.Queue.ProcessNextAsync());
|
Assert.True(await fixture.Queue.ProcessNextAsync());
|
||||||
Assert.Equal([(job.SourceVideoPath, job.TargetVideoPath)], fixture.OpenList.CopyRequests);
|
Assert.Equal([(job.SourceVideoPath, job.TargetVideoPath)], fixture.OpenList.CopyRequests);
|
||||||
|
Assert.Equal(["/destination/Douyin/2026/08/01/主播"], fixture.OpenList.EnsuredDirectories);
|
||||||
|
Assert.True(
|
||||||
|
fixture.OpenList.Operations.IndexOf("ensure:/destination/Douyin/2026/08/01/主播") <
|
||||||
|
fixture.OpenList.Operations.IndexOf($"get:{job.TargetVideoPath}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -575,6 +611,10 @@ public sealed class OpenListUploadTests
|
|||||||
|
|
||||||
public List<(string Source, string Target)> CopyRequests { get; } = [];
|
public List<(string Source, string Target)> CopyRequests { get; } = [];
|
||||||
|
|
||||||
|
public List<string> EnsuredDirectories { get; } = [];
|
||||||
|
|
||||||
|
public List<string> Operations { get; } = [];
|
||||||
|
|
||||||
public OpenListCopyResult NextCopyResult { get; set; } = new([]);
|
public OpenListCopyResult NextCopyResult { get; set; } = new([]);
|
||||||
|
|
||||||
public Task<OpenListConnectionTestDto> TestConnectionAsync(
|
public Task<OpenListConnectionTestDto> TestConnectionAsync(
|
||||||
@@ -588,13 +628,21 @@ public sealed class OpenListUploadTests
|
|||||||
public Task EnsureDirectoryAsync(
|
public Task EnsureDirectoryAsync(
|
||||||
OpenListConnectionRequest connection,
|
OpenListConnectionRequest connection,
|
||||||
string path,
|
string path,
|
||||||
CancellationToken cancellationToken = default) => Task.CompletedTask;
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
EnsuredDirectories.Add(path);
|
||||||
|
Operations.Add($"ensure:{path}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<OpenListObjectInfo?> TryGetObjectAsync(
|
public Task<OpenListObjectInfo?> TryGetObjectAsync(
|
||||||
OpenListConnectionRequest connection,
|
OpenListConnectionRequest connection,
|
||||||
string path,
|
string path,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default)
|
||||||
Task.FromResult(Objects.GetValueOrDefault(path));
|
{
|
||||||
|
Operations.Add($"get:{path}");
|
||||||
|
return Task.FromResult(Objects.GetValueOrDefault(path));
|
||||||
|
}
|
||||||
|
|
||||||
public Task<OpenListCopyResult> CopyFileAsync(
|
public Task<OpenListCopyResult> CopyFileAsync(
|
||||||
OpenListConnectionRequest connection,
|
OpenListConnectionRequest connection,
|
||||||
@@ -602,6 +650,7 @@ public sealed class OpenListUploadTests
|
|||||||
string targetPath,
|
string targetPath,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
Operations.Add($"copy:{sourcePath}->{targetPath}");
|
||||||
CopyRequests.Add((sourcePath, targetPath));
|
CopyRequests.Add((sourcePath, targetPath));
|
||||||
return Task.FromResult(NextCopyResult);
|
return Task.FromResult(NextCopyResult);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user