Add a job to backfill attachment uploads to the archive service.

This commit is contained in:
Greyson Parrelli
2024-04-18 11:23:58 -04:00
parent 1e4d96b7c4
commit a82b9ee25f
17 changed files with 567 additions and 113 deletions

View File

@@ -6,7 +6,6 @@
package org.whispersystems.signalservice.api
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
import java.io.IOException
/**
@@ -33,7 +32,7 @@ sealed class NetworkResult<T> {
fun <T> fromFetch(fetch: () -> T): NetworkResult<T> = try {
Success(fetch())
} catch (e: NonSuccessfulResponseCodeException) {
StatusCodeError(e.code, e)
StatusCodeError(e.code, e.body, e)
} catch (e: IOException) {
NetworkError(e)
} catch (e: Throwable) {
@@ -45,10 +44,10 @@ sealed class NetworkResult<T> {
data class Success<T>(val result: T) : NetworkResult<T>()
/** Indicates a generic network error occurred before we were able to process a response. */
data class NetworkError<T>(val throwable: Throwable? = null) : NetworkResult<T>()
data class NetworkError<T>(val exception: IOException) : NetworkResult<T>()
/** Indicates we got a response, but it was a non-2xx response. */
data class StatusCodeError<T>(val code: Int, val throwable: Throwable? = null) : NetworkResult<T>()
data class StatusCodeError<T>(val code: Int, val body: String?, val exception: IOException) : NetworkResult<T>()
/** Indicates that the application somehow failed in a way unrelated to network activity. Usually a runtime crash. */
data class ApplicationError<T>(val throwable: Throwable) : NetworkResult<T>()
@@ -59,8 +58,8 @@ sealed class NetworkResult<T> {
fun successOrThrow(): T {
when (this) {
is Success -> return result
is NetworkError -> throw throwable ?: PushNetworkException("Network error")
is StatusCodeError -> throw throwable ?: NonSuccessfulResponseCodeException(this.code)
is NetworkError -> throw exception
is StatusCodeError -> throw exception
is ApplicationError -> throw throwable
}
}
@@ -72,8 +71,8 @@ sealed class NetworkResult<T> {
fun <R> map(transform: (T) -> R): NetworkResult<R> {
return when (this) {
is Success -> Success(transform(this.result))
is NetworkError -> NetworkError(throwable)
is StatusCodeError -> StatusCodeError(code, throwable)
is NetworkError -> NetworkError(exception)
is StatusCodeError -> StatusCodeError(code, body, exception)
is ApplicationError -> ApplicationError(throwable)
}
}
@@ -85,8 +84,8 @@ sealed class NetworkResult<T> {
fun <R> then(result: (T) -> NetworkResult<R>): NetworkResult<R> {
return when (this) {
is Success -> result(this.result)
is NetworkError -> NetworkError(throwable)
is StatusCodeError -> StatusCodeError(code, throwable)
is NetworkError -> NetworkError(exception)
is StatusCodeError -> StatusCodeError(code, body, exception)
is ApplicationError -> ApplicationError(throwable)
}
}

View File

@@ -176,6 +176,13 @@ class ArchiveApi(
/**
* Copy and re-encrypt media from the attachments cdn into the backup cdn.
*
* Possible errors:
* 400: Bad arguments, or made on an authenticated channel
* 401: Invalid presentation or signature
* 403: Insufficient permissions
* 413: No media space remaining
* 429: Rate-limited
*/
fun archiveAttachmentMedia(
backupKey: BackupKey,

View File

@@ -12,4 +12,19 @@ import com.fasterxml.jackson.annotation.JsonProperty
*/
class ArchiveMediaResponse(
@JsonProperty val cdn: Int
)
) {
enum class StatusCodes(val code: Int) {
BadArguments(400),
InvalidPresentationOrSignature(401),
InsufficientPermissions(403),
NoMediaSpaceRemaining(413),
RateLimited(429),
Unknown(-1);
companion object {
fun from(code: Int): StatusCodes {
return values().firstOrNull { it.code == code } ?: Unknown
}
}
}
}

View File

@@ -13,16 +13,25 @@ import java.io.IOException;
*/
public class NonSuccessfulResponseCodeException extends IOException {
private final int code;
private final int code;
private final String body;
public NonSuccessfulResponseCodeException(int code) {
super("StatusCode: " + code);
this.code = code;
this.body = null;
}
public NonSuccessfulResponseCodeException(int code, String s) {
super("[" + code + "] " + s);
this.code = code;
this.body = null;
}
public NonSuccessfulResponseCodeException(int code, String s, String body) {
super("[" + code + "] " + s);
this.code = code;
this.body = body;
}
public int getCode() {
@@ -36,4 +45,8 @@ public class NonSuccessfulResponseCodeException extends IOException {
public boolean is5xx() {
return code >= 500 && code < 600;
}
public String getBody() {
return body;
}
}

View File

@@ -325,8 +325,9 @@ public class PushServiceSocket {
private static final String CALL_LINK_CREATION_AUTH = "/v1/call-link/create-auth";
private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp";
private static final Map<String, String> NO_HEADERS = Collections.emptyMap();
private static final ResponseCodeHandler NO_HANDLER = new EmptyResponseCodeHandler();
private static final Map<String, String> NO_HEADERS = Collections.emptyMap();
private static final ResponseCodeHandler NO_HANDLER = new EmptyResponseCodeHandler();
private static final ResponseCodeHandler UNOPINIONATED_HANDER = new UnopinionatedResponseCodeHandler();
private static final long CDN2_RESUMABLE_LINK_LIFETIME_MILLIS = TimeUnit.DAYS.toMillis(7);
@@ -494,14 +495,14 @@ public class PushServiceSocket {
long secondsRoundedToNearestDay = TimeUnit.DAYS.toSeconds(TimeUnit.MILLISECONDS.toDays(currentTime));
long endTimeInSeconds = secondsRoundedToNearestDay + TimeUnit.DAYS.toSeconds(7);
String response = makeServiceRequest(String.format(Locale.US, ARCHIVE_CREDENTIALS, secondsRoundedToNearestDay, endTimeInSeconds), "GET", null);
String response = makeServiceRequest(String.format(Locale.US, ARCHIVE_CREDENTIALS, secondsRoundedToNearestDay, endTimeInSeconds), "GET", null, NO_HEADERS, UNOPINIONATED_HANDER, Optional.empty());
return JsonUtil.fromJson(response, ArchiveServiceCredentialsResponse.class);
}
public void setArchiveBackupId(BackupAuthCredentialRequest request) throws IOException {
String body = JsonUtil.toJson(new ArchiveSetBackupIdRequest(request));
makeServiceRequest(ARCHIVE_BACKUP_ID, "PUT", body);
makeServiceRequest(ARCHIVE_BACKUP_ID, "PUT", body, NO_HEADERS, UNOPINIONATED_HANDER, Optional.empty());
}
public void setArchivePublicKey(ECPublicKey publicKey, ArchiveCredentialPresentation credentialPresentation) throws IOException {
@@ -555,7 +556,7 @@ public class PushServiceSocket {
public ArchiveMediaResponse archiveAttachmentMedia(@Nonnull ArchiveCredentialPresentation credentialPresentation, @Nonnull ArchiveMediaRequest request) throws IOException {
Map<String, String> headers = credentialPresentation.toHeaders();
String response = makeServiceRequestWithoutAuthentication(ARCHIVE_MEDIA, "PUT", JsonUtil.toJson(request), headers, NO_HANDLER);
String response = makeServiceRequestWithoutAuthentication(ARCHIVE_MEDIA, "PUT", JsonUtil.toJson(request), headers, UNOPINIONATED_HANDER);
return JsonUtil.fromJson(response, ArchiveMediaResponse.class);
}
@@ -566,7 +567,7 @@ public class PushServiceSocket {
public BatchArchiveMediaResponse archiveAttachmentMedia(@Nonnull ArchiveCredentialPresentation credentialPresentation, @Nonnull BatchArchiveMediaRequest request) throws IOException {
Map<String, String> headers = credentialPresentation.toHeaders();
String response = makeServiceRequestWithoutAuthentication(ARCHIVE_MEDIA_BATCH, "PUT", JsonUtil.toJson(request), headers, NO_HANDLER);
String response = makeServiceRequestWithoutAuthentication(ARCHIVE_MEDIA_BATCH, "PUT", JsonUtil.toJson(request), headers, UNOPINIONATED_HANDER);
return JsonUtil.fromJson(response, BatchArchiveMediaResponse.class);
}
@@ -2660,6 +2661,28 @@ public class PushServiceSocket {
public void handle(int responseCode, ResponseBody body) { }
}
/**
* A {@link ResponseCodeHandler} that only throws {@link NonSuccessfulResponseCodeException} with the response body.
* Any further processing is left to the caller.
*/
private static class UnopinionatedResponseCodeHandler implements ResponseCodeHandler {
@Override
public void handle(int responseCode, ResponseBody body) throws NonSuccessfulResponseCodeException, PushNetworkException {
if (responseCode < 200 || responseCode > 299) {
String bodyString = null;
if (body != null) {
try {
bodyString = readBodyString(body);
} catch (MalformedResponseException e) {
Log.w(TAG, "Failed to read body string", e);
}
}
throw new NonSuccessfulResponseCodeException(responseCode, "Response: " + responseCode, bodyString);
}
}
}
public enum ClientSet { KeyBackup }
public CredentialResponse retrieveGroupsV2Credentials(long todaySeconds)