upload client name and server address as description
This commit is contained in:
@@ -14,10 +14,12 @@ public record UploadRequest(
|
||||
String fileName,
|
||||
Instant capturedAt,
|
||||
Instant modifiedAt,
|
||||
AlbumRef album) {
|
||||
AlbumRef album,
|
||||
String description) {
|
||||
|
||||
public UploadRequest {
|
||||
album = album == null ? AlbumRef.library() : album;
|
||||
modifiedAt = modifiedAt == null ? capturedAt : modifiedAt;
|
||||
description = description == null ? "" : description;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,19 @@ final class ImmichApi {
|
||||
return decode(send(request), type);
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCHes JSON and discards the response body -- the only PATCH PhotoSync
|
||||
* makes is the asset description update, whose reply is the whole asset we
|
||||
* do not need.
|
||||
*/
|
||||
void patch(String path, Object body) throws ProviderException {
|
||||
HttpRequest request = request(path, Map.of(), REQUEST_TIMEOUT)
|
||||
.header("Content-Type", "application/json")
|
||||
.method("PATCH", HttpRequest.BodyPublishers.ofString(gson.toJson(body), StandardCharsets.UTF_8))
|
||||
.build();
|
||||
send(request);
|
||||
}
|
||||
|
||||
<T> T upload(String path, MultipartBody body, String checksumBase64, TransferProgress progress, Type type)
|
||||
throws ProviderException {
|
||||
HttpRequest.Builder builder = request(path, Map.of(), UPLOAD_TIMEOUT)
|
||||
|
||||
@@ -43,6 +43,10 @@ final class ImmichDtos {
|
||||
record AssetMediaResponse(String id, String status) {
|
||||
}
|
||||
|
||||
/** Body for {@code PATCH /assets/:id} -- the only field PhotoSync writes. */
|
||||
record UpdateAsset(String description) {
|
||||
}
|
||||
|
||||
record BulkIds(List<String> ids) {
|
||||
}
|
||||
|
||||
|
||||
@@ -161,6 +161,9 @@ public final class ImmichProvider implements PhotoProvider {
|
||||
if (album.isPresent()) {
|
||||
addToAlbum(album.get(), response.id());
|
||||
}
|
||||
if (!request.description().isBlank()) {
|
||||
setDescription(response.id(), request.description());
|
||||
}
|
||||
return new UploadReceipt(response.id(), outcome);
|
||||
}
|
||||
|
||||
@@ -327,6 +330,24 @@ public final class ImmichProvider implements PhotoProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the one-line description after the asset exists, even when the
|
||||
* upload answered {@code duplicate} -- a retried upload whose first
|
||||
* response was lost reaches the server twice, and the description must land
|
||||
* on whichever attempt gets there.
|
||||
*
|
||||
* <p>Failures are warnings, not errors: the screenshot is the point, and the
|
||||
* queue must not show a failed upload for an asset the server is holding.
|
||||
*/
|
||||
private void setDescription(String assetId, String description) {
|
||||
try {
|
||||
api.patch("/assets/" + assetId, new ImmichDtos.UpdateAsset(description));
|
||||
} catch (ProviderException e) {
|
||||
log.warn("Uploaded {} but Immich would not set its description: {}",
|
||||
assetId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** Album filter plus the flags that keep archived and trashed assets out of the library view. */
|
||||
private static Map<String, String> scope(AlbumRef album) {
|
||||
Map<String, String> query = new LinkedHashMap<>();
|
||||
|
||||
@@ -29,6 +29,7 @@ public record UploadJob(
|
||||
Instant capturedAt,
|
||||
CaptureOrigin origin,
|
||||
String albumId,
|
||||
String description,
|
||||
UploadState state,
|
||||
int attempts,
|
||||
Instant notBefore,
|
||||
@@ -48,7 +49,7 @@ public record UploadJob(
|
||||
}
|
||||
|
||||
public UploadRequest toRequest() {
|
||||
return new UploadRequest(path(), fileName, capturedAt, capturedAt, album());
|
||||
return new UploadRequest(path(), fileName, capturedAt, capturedAt, album(), description);
|
||||
}
|
||||
|
||||
/** True once the backoff has elapsed and a worker may pick this up. */
|
||||
|
||||
@@ -123,8 +123,12 @@ public final class UploadQueue {
|
||||
* <p>The de-duplication matters on the path where a capture is enqueued and
|
||||
* the player immediately quits and relaunches: the restored job and a fresh
|
||||
* rescan would otherwise both try to upload the same file.
|
||||
*
|
||||
* @param description one line of context captured with the screenshot, e.g.
|
||||
* client and server; persisted with the job so a retry
|
||||
* after a restart describes the same moment
|
||||
*/
|
||||
public UploadJob enqueue(CapturedScreenshot shot, AlbumRef album) {
|
||||
public UploadJob enqueue(CapturedScreenshot shot, AlbumRef album, String description) {
|
||||
String absolute = shot.file().toAbsolutePath().toString();
|
||||
UploadJob created;
|
||||
synchronized (lock) {
|
||||
@@ -143,6 +147,7 @@ public final class UploadQueue {
|
||||
.capturedAt(shot.capturedAt())
|
||||
.origin(shot.origin())
|
||||
.albumId(album.id().orElse(""))
|
||||
.description(description)
|
||||
.state(UploadState.PENDING)
|
||||
.attempts(0)
|
||||
.build();
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package dev.photosync.core.upload;
|
||||
|
||||
import dev.photosync.core.provider.UploadRequest;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* The description is captured once, at the moment the screenshot is taken, and
|
||||
* must survive the queue's persistence and retries unchanged -- it describes
|
||||
* the moment the shot was taken, not the moment it was sent.
|
||||
*/
|
||||
class UploadJobTest {
|
||||
|
||||
private static final Instant TAKEN = Instant.parse("2026-08-08T12:00:00Z");
|
||||
|
||||
@Test
|
||||
@DisplayName("the context captured at enqueue time travels with the upload")
|
||||
void descriptionSurvivesToRequest() {
|
||||
UploadJob job = UploadJob.builder()
|
||||
.id("job-1")
|
||||
.file("/tmp/shot.png")
|
||||
.fileName("shot.png")
|
||||
.sizeBytes(42)
|
||||
.capturedAt(TAKEN)
|
||||
.description("My Pack · play.example.com")
|
||||
.state(UploadState.PENDING)
|
||||
.attempts(0)
|
||||
.build();
|
||||
|
||||
assertEquals("My Pack · play.example.com", job.toRequest().description());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a job restored from an old queue file has no description to send")
|
||||
void nullDescriptionBecomesEmpty() {
|
||||
UploadRequest request = new UploadRequest(
|
||||
Path.of("/tmp/shot.png"), "shot.png", TAKEN, TAKEN, null, null);
|
||||
|
||||
assertEquals("", request.description());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user