mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-20 02:28:03 +01:00
Add support for distinct media backup credentials
Co-authored-by: Ravi Khadiwala <ravi@signal.org>
This commit is contained in:
@@ -21,6 +21,7 @@ import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupCredentialType;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupLevel;
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation;
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptSerial;
|
||||
@@ -29,7 +30,6 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
@@ -81,22 +81,34 @@ public class BackupAuthManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a credential request containing a blinded backup-id for future use.
|
||||
* Store credential requests containing blinded backup-ids for future use.
|
||||
*
|
||||
* @param account The account using the backup-id
|
||||
* @param backupAuthCredentialRequest A request containing the blinded backup-id
|
||||
* @param account The account using the backup-id
|
||||
* @param messagesBackupCredentialRequest A request containing the blinded backup-id the client will use to upload
|
||||
* message backups
|
||||
* @param mediaBackupCredentialRequest A request containing the blinded backup-id the client will use to upload
|
||||
* media backups
|
||||
* @return A future that completes when the credentialRequest has been stored
|
||||
* @throws RateLimitExceededException If too many backup-ids have been committed
|
||||
*/
|
||||
public CompletableFuture<Void> commitBackupId(final Account account,
|
||||
final BackupAuthCredentialRequest backupAuthCredentialRequest) {
|
||||
final BackupAuthCredentialRequest messagesBackupCredentialRequest,
|
||||
final BackupAuthCredentialRequest mediaBackupCredentialRequest) {
|
||||
if (configuredBackupLevel(account).isEmpty()) {
|
||||
throw Status.PERMISSION_DENIED.withDescription("Backups not allowed on account").asRuntimeException();
|
||||
}
|
||||
final byte[] serializedMessageCredentialRequest = messagesBackupCredentialRequest.serialize();
|
||||
final byte[] serializedMediaCredentialRequest = mediaBackupCredentialRequest.serialize();
|
||||
|
||||
byte[] serializedRequest = backupAuthCredentialRequest.serialize();
|
||||
byte[] existingRequest = account.getBackupCredentialRequest();
|
||||
if (existingRequest != null && MessageDigest.isEqual(serializedRequest, existingRequest)) {
|
||||
final boolean messageCredentialRequestMatches = account.getBackupCredentialRequest(BackupCredentialType.MESSAGES)
|
||||
.map(storedCredentialRequest -> MessageDigest.isEqual(storedCredentialRequest, serializedMessageCredentialRequest))
|
||||
.orElse(false);
|
||||
|
||||
final boolean mediaCredentialRequestMatches = account.getBackupCredentialRequest(BackupCredentialType.MEDIA)
|
||||
.map(storedCredentialRequest -> MessageDigest.isEqual(storedCredentialRequest, serializedMediaCredentialRequest))
|
||||
.orElse(false);
|
||||
|
||||
if (messageCredentialRequestMatches && mediaCredentialRequestMatches) {
|
||||
// No need to update or enforce rate limits, this is the credential that the user has already
|
||||
// committed to.
|
||||
return CompletableFuture.completedFuture(null);
|
||||
@@ -105,7 +117,7 @@ public class BackupAuthManager {
|
||||
return rateLimiters.forDescriptor(RateLimiters.For.SET_BACKUP_ID)
|
||||
.validateAsync(account.getUuid())
|
||||
.thenCompose(ignored -> this.accountsManager
|
||||
.updateAsync(account, acc -> acc.setBackupCredentialRequest(serializedRequest))
|
||||
.updateAsync(account, a -> a.setBackupCredentialRequests(serializedMessageCredentialRequest, serializedMediaCredentialRequest))
|
||||
.thenRun(Util.NOOP))
|
||||
.toCompletableFuture();
|
||||
}
|
||||
@@ -123,12 +135,14 @@ public class BackupAuthManager {
|
||||
* method will also remove the expired voucher from the account.
|
||||
*
|
||||
* @param account The account to create the credentials for
|
||||
* @param credentialType The type of backup credentials to create
|
||||
* @param redemptionStart The day (must be truncated to a day boundary) the first credential should be valid
|
||||
* @param redemptionEnd The day (must be truncated to a day boundary) the last credential should be valid
|
||||
* @return Credentials and the day on which they may be redeemed
|
||||
*/
|
||||
public CompletableFuture<List<Credential>> getBackupAuthCredentials(
|
||||
final Account account,
|
||||
final BackupCredentialType credentialType,
|
||||
final Instant redemptionStart,
|
||||
final Instant redemptionEnd) {
|
||||
|
||||
@@ -139,7 +153,7 @@ public class BackupAuthManager {
|
||||
if (hasExpiredVoucher(a)) {
|
||||
a.setBackupVoucher(null);
|
||||
}
|
||||
}).thenCompose(updated -> getBackupAuthCredentials(updated, redemptionStart, redemptionEnd));
|
||||
}).thenCompose(updated -> getBackupAuthCredentials(updated, credentialType, redemptionStart, redemptionEnd));
|
||||
}
|
||||
|
||||
// If this account isn't allowed some level of backup access via configuration, don't continue
|
||||
@@ -157,23 +171,20 @@ public class BackupAuthManager {
|
||||
}
|
||||
|
||||
// fetch the blinded backup-id the account should have previously committed to
|
||||
final byte[] committedBytes = account.getBackupCredentialRequest();
|
||||
if (committedBytes == null) {
|
||||
throw Status.NOT_FOUND.withDescription("No blinded backup-id has been added to the account").asRuntimeException();
|
||||
}
|
||||
final byte[] committedBytes = account.getBackupCredentialRequest(credentialType)
|
||||
.orElseThrow(() -> Status.NOT_FOUND.withDescription("No blinded backup-id has been added to the account").asRuntimeException());
|
||||
|
||||
try {
|
||||
// create a credential for every day in the requested period
|
||||
final BackupAuthCredentialRequest credentialReq = new BackupAuthCredentialRequest(committedBytes);
|
||||
return CompletableFuture.completedFuture(Stream
|
||||
.iterate(redemptionStart, curr -> curr.plus(Duration.ofDays(1)))
|
||||
.takeWhile(redemptionTime -> !redemptionTime.isAfter(redemptionEnd))
|
||||
.iterate(redemptionStart, redemptionTime -> !redemptionTime.isAfter(redemptionEnd), curr -> curr.plus(Duration.ofDays(1)))
|
||||
.map(redemptionTime -> {
|
||||
// Check if the account has a voucher that's good for a certain receiptLevel at redemption time, otherwise
|
||||
// use the default receipt level
|
||||
final BackupLevel backupLevel = storedBackupLevel(account, redemptionTime).orElse(configuredBackupLevel);
|
||||
return new Credential(
|
||||
credentialReq.issueCredential(redemptionTime, backupLevel, serverSecretParams),
|
||||
credentialReq.issueCredential(redemptionTime, backupLevel, credentialType, serverSecretParams),
|
||||
redemptionTime);
|
||||
})
|
||||
.toList());
|
||||
@@ -210,7 +221,7 @@ public class BackupAuthManager {
|
||||
|
||||
final long receiptLevel = receiptCredentialPresentation.getReceiptLevel();
|
||||
|
||||
if (BackupLevelUtil.fromReceiptLevel(receiptLevel) != BackupLevel.MEDIA) {
|
||||
if (BackupLevelUtil.fromReceiptLevel(receiptLevel) != BackupLevel.PAID) {
|
||||
throw Status.INVALID_ARGUMENT
|
||||
.withDescription("server does not recognize the requested receipt level")
|
||||
.asRuntimeException();
|
||||
@@ -281,10 +292,10 @@ public class BackupAuthManager {
|
||||
*/
|
||||
private Optional<BackupLevel> configuredBackupLevel(final Account account) {
|
||||
if (inExperiment(BACKUP_MEDIA_EXPERIMENT_NAME, account)) {
|
||||
return Optional.of(BackupLevel.MEDIA);
|
||||
return Optional.of(BackupLevel.PAID);
|
||||
}
|
||||
if (inExperiment(BACKUP_EXPERIMENT_NAME, account)) {
|
||||
return Optional.of(BackupLevel.MESSAGES);
|
||||
return Optional.of(BackupLevel.FREE);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -28,25 +28,22 @@ import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupCredentialType;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupLevel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.attachments.AttachmentGenerator;
|
||||
import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.util.AsyncTimerUtil;
|
||||
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
|
||||
public class BackupManager {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(BackupManager.class);
|
||||
|
||||
static final String MESSAGE_BACKUP_NAME = "messageBackup";
|
||||
public static final long MAX_TOTAL_BACKUP_MEDIA_BYTES = DataSize.gibibytes(100).toBytes();
|
||||
static final long MAX_MEDIA_OBJECT_SIZE = DataSize.mebibytes(101).toBytes();
|
||||
@@ -120,8 +117,10 @@ public class BackupManager {
|
||||
// Note: this is a special case where we can't validate the presentation signature against the stored public key
|
||||
// because we are currently setting it. We check against the provided public key, but we must also verify that
|
||||
// there isn't an existing, different stored public key for the backup-id (verified with a condition expression)
|
||||
final BackupLevel backupLevel = verifyPresentation(presentation).verifySignature(signature, publicKey);
|
||||
return backupsDb.setPublicKey(presentation.getBackupId(), backupLevel, publicKey)
|
||||
final Pair<BackupCredentialType, BackupLevel> credentialTypeAndBackupLevel =
|
||||
verifyPresentation(presentation).verifySignature(signature, publicKey);
|
||||
|
||||
return backupsDb.setPublicKey(presentation.getBackupId(), credentialTypeAndBackupLevel.second(), publicKey)
|
||||
.exceptionally(ExceptionUtils.exceptionallyHandler(PublicKeyConflictException.class, ex -> {
|
||||
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
|
||||
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||
@@ -144,7 +143,8 @@ public class BackupManager {
|
||||
*/
|
||||
public CompletableFuture<BackupUploadDescriptor> createMessageBackupUploadDescriptor(
|
||||
final AuthenticatedBackupUser backupUser) {
|
||||
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
|
||||
checkBackupLevel(backupUser, BackupLevel.FREE);
|
||||
checkBackupCredentialType(backupUser, BackupCredentialType.MESSAGES);
|
||||
|
||||
// this could race with concurrent updates, but the only effect would be last-writer-wins on the timestamp
|
||||
return backupsDb
|
||||
@@ -154,7 +154,8 @@ public class BackupManager {
|
||||
|
||||
public CompletableFuture<BackupUploadDescriptor> createTemporaryAttachmentUploadDescriptor(
|
||||
final AuthenticatedBackupUser backupUser) {
|
||||
checkBackupLevel(backupUser, BackupLevel.MEDIA);
|
||||
checkBackupLevel(backupUser, BackupLevel.PAID);
|
||||
checkBackupCredentialType(backupUser, BackupCredentialType.MEDIA);
|
||||
|
||||
return rateLimiters.forDescriptor(RateLimiters.For.BACKUP_ATTACHMENT)
|
||||
.validateAsync(rateLimitKey(backupUser)).thenApply(ignored -> {
|
||||
@@ -172,7 +173,7 @@ public class BackupManager {
|
||||
* @param backupUser an already ZK authenticated backup user
|
||||
*/
|
||||
public CompletableFuture<Void> ttlRefresh(final AuthenticatedBackupUser backupUser) {
|
||||
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
|
||||
checkBackupLevel(backupUser, BackupLevel.FREE);
|
||||
// update message backup TTL
|
||||
return backupsDb.ttlRefresh(backupUser);
|
||||
}
|
||||
@@ -187,7 +188,7 @@ public class BackupManager {
|
||||
* @return Information about the existing backup
|
||||
*/
|
||||
public CompletableFuture<BackupInfo> backupInfo(final AuthenticatedBackupUser backupUser) {
|
||||
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
|
||||
checkBackupLevel(backupUser, BackupLevel.FREE);
|
||||
return backupsDb.describeBackup(backupUser)
|
||||
.thenApply(backupDescription -> new BackupInfo(
|
||||
backupDescription.cdn(),
|
||||
@@ -210,7 +211,8 @@ public class BackupManager {
|
||||
* detailing why the object could not be copied.
|
||||
*/
|
||||
public Flux<CopyResult> copyToBackup(final AuthenticatedBackupUser backupUser, List<CopyParameters> toCopy) {
|
||||
checkBackupLevel(backupUser, BackupLevel.MEDIA);
|
||||
checkBackupLevel(backupUser, BackupLevel.PAID);
|
||||
checkBackupCredentialType(backupUser, BackupCredentialType.MEDIA);
|
||||
|
||||
return Mono
|
||||
// Figure out how many objects we're allowed to copy, updating the quota usage for the amount we are allowed
|
||||
@@ -349,7 +351,7 @@ public class BackupManager {
|
||||
* @return A map of headers to include with CDN requests
|
||||
*/
|
||||
public Map<String, String> generateReadAuth(final AuthenticatedBackupUser backupUser, final int cdnNumber) {
|
||||
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
|
||||
checkBackupLevel(backupUser, BackupLevel.FREE);
|
||||
if (cdnNumber != 3) {
|
||||
throw Status.INVALID_ARGUMENT.withDescription("unknown cdn").asRuntimeException();
|
||||
}
|
||||
@@ -377,7 +379,7 @@ public class BackupManager {
|
||||
final AuthenticatedBackupUser backupUser,
|
||||
final Optional<String> cursor,
|
||||
final int limit) {
|
||||
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
|
||||
checkBackupLevel(backupUser, BackupLevel.FREE);
|
||||
return remoteStorageManager.list(cdnMediaDirectory(backupUser), cursor, limit)
|
||||
.thenApply(result ->
|
||||
new ListMediaResult(
|
||||
@@ -395,7 +397,7 @@ public class BackupManager {
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> deleteEntireBackup(final AuthenticatedBackupUser backupUser) {
|
||||
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
|
||||
checkBackupLevel(backupUser, BackupLevel.FREE);
|
||||
return backupsDb
|
||||
// Try to swap out the backupDir for the user
|
||||
.scheduleBackupDeletion(backupUser)
|
||||
@@ -408,7 +410,8 @@ public class BackupManager {
|
||||
|
||||
public Flux<StorageDescriptor> deleteMedia(final AuthenticatedBackupUser backupUser,
|
||||
final List<StorageDescriptor> storageDescriptors) {
|
||||
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
|
||||
checkBackupLevel(backupUser, BackupLevel.FREE);
|
||||
checkBackupCredentialType(backupUser, BackupCredentialType.MEDIA);
|
||||
|
||||
// Check for a cdn we don't know how to process
|
||||
if (storageDescriptors.stream().anyMatch(sd -> sd.cdn() != remoteStorageManager.cdnNumber())) {
|
||||
@@ -492,10 +495,16 @@ public class BackupManager {
|
||||
// There was no stored public key, use a bunk public key so that validation will fail
|
||||
return new BackupsDb.AuthenticationData(INVALID_PUBLIC_KEY, null, null);
|
||||
});
|
||||
|
||||
final Pair<BackupCredentialType, BackupLevel> credentialTypeAndBackupLevel =
|
||||
signatureVerifier.verifySignature(signature, authenticationData.publicKey());
|
||||
|
||||
return new AuthenticatedBackupUser(
|
||||
presentation.getBackupId(),
|
||||
signatureVerifier.verifySignature(signature, authenticationData.publicKey()),
|
||||
authenticationData.backupDir(), authenticationData.mediaDir());
|
||||
credentialTypeAndBackupLevel.first(),
|
||||
credentialTypeAndBackupLevel.second(),
|
||||
authenticationData.backupDir(),
|
||||
authenticationData.mediaDir());
|
||||
})
|
||||
.thenApply(result -> {
|
||||
Metrics.counter(ZK_AUTHN_COUNTER_NAME, SUCCESS_TAG_NAME, String.valueOf(true)).increment();
|
||||
@@ -579,7 +588,7 @@ public class BackupManager {
|
||||
|
||||
interface PresentationSignatureVerifier {
|
||||
|
||||
BackupLevel verifySignature(byte[] signature, ECPublicKey publicKey);
|
||||
Pair<BackupCredentialType, BackupLevel> verifySignature(byte[] signature, ECPublicKey publicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -611,7 +620,7 @@ public class BackupManager {
|
||||
.withDescription("backup auth credential presentation signature verification failed")
|
||||
.asRuntimeException();
|
||||
}
|
||||
return presentation.getBackupLevel();
|
||||
return new Pair<>(presentation.getType(), presentation.getBackupLevel());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -622,9 +631,34 @@ public class BackupManager {
|
||||
* @param backupLevel The authorization level to verify the backupUser has access to
|
||||
* @throws {@link Status#PERMISSION_DENIED} error if the backup user is not authorized to access {@code backupLevel}
|
||||
*/
|
||||
private static void checkBackupLevel(final AuthenticatedBackupUser backupUser, final BackupLevel backupLevel) {
|
||||
@VisibleForTesting
|
||||
static void checkBackupLevel(final AuthenticatedBackupUser backupUser, final BackupLevel backupLevel) {
|
||||
if (backupUser.backupLevel().compareTo(backupLevel) < 0) {
|
||||
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
|
||||
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME,
|
||||
FAILURE_REASON_TAG_NAME, "level")
|
||||
.increment();
|
||||
|
||||
throw Status.PERMISSION_DENIED
|
||||
.withDescription("credential does not support the requested operation")
|
||||
.asRuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the authenticated backup user is authenticated with the given credential type
|
||||
*
|
||||
* @param backupUser The backup user to check
|
||||
* @param credentialType The credential type to require
|
||||
* @throws {@link Status#PERMISSION_DENIED} error if the backup user is not authenticated with the given
|
||||
* {@code credentialType}
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static void checkBackupCredentialType(final AuthenticatedBackupUser backupUser, final BackupCredentialType credentialType) {
|
||||
if (backupUser.credentialType() != credentialType) {
|
||||
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME,
|
||||
FAILURE_REASON_TAG_NAME, "credential_type")
|
||||
.increment();
|
||||
|
||||
throw Status.PERMISSION_DENIED
|
||||
.withDescription("credential does not support the requested operation")
|
||||
.asRuntimeException();
|
||||
|
||||
@@ -87,7 +87,7 @@ public class BackupsDb {
|
||||
// garbage collection of archive objects.
|
||||
public static final String ATTR_LAST_REFRESH = "R";
|
||||
// N: Time in seconds since epoch of the last backup media refresh. This timestamp can only be updated if the client
|
||||
// has BackupLevel.MEDIA, and must be periodically updated to avoid garbage collection of media objects.
|
||||
// has BackupLevel.PAID, and must be periodically updated to avoid garbage collection of media objects.
|
||||
public static final String ATTR_LAST_MEDIA_REFRESH = "MR";
|
||||
// B: A 32 byte public key that should be used to sign the presentation used to authenticate requests against the
|
||||
// backup-id
|
||||
@@ -265,7 +265,7 @@ public class BackupsDb {
|
||||
* Indicates that we couldn't schedule a deletion because one was already scheduled. The caller may want to delete the
|
||||
* objects directly.
|
||||
*/
|
||||
class PendingDeletionException extends IOException {}
|
||||
static class PendingDeletionException extends IOException {}
|
||||
|
||||
/**
|
||||
* Attempt to mark a backup as expired and swap in a new empty backupDir for the user.
|
||||
@@ -285,7 +285,7 @@ public class BackupsDb {
|
||||
final byte[] hashedBackupId = hashedBackupId(backupUser);
|
||||
|
||||
// Clear usage metadata, swap names of things we intend to delete, and record our intent to delete in attr_expired_prefix
|
||||
return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupLevel.MEDIA, hashedBackupId)
|
||||
return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupLevel.PAID, hashedBackupId)
|
||||
.clearMediaUsage(clock)
|
||||
.expireDirectoryNames(secureRandom, ExpiredBackup.ExpirationType.ALL)
|
||||
.setRefreshTimes(Instant.ofEpochSecond(0))
|
||||
@@ -300,7 +300,7 @@ public class BackupsDb {
|
||||
// is toggling backups on and off. In this case, it should be pretty cheap to directly delete the backup.
|
||||
// Instead of changing the backupDir, just make sure the row has expired/ timestamps and tell the caller we
|
||||
// couldn't schedule the deletion.
|
||||
dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupLevel.MEDIA, hashedBackupId)
|
||||
dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupLevel.PAID, hashedBackupId)
|
||||
.setRefreshTimes(Instant.ofEpochSecond(0))
|
||||
.updateItemBuilder()
|
||||
.build())
|
||||
@@ -399,7 +399,7 @@ public class BackupsDb {
|
||||
}
|
||||
|
||||
// Clear usage metadata, swap names of things we intend to delete, and record our intent to delete in attr_expired_prefix
|
||||
return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupLevel.MEDIA, expiredBackup.hashedBackupId())
|
||||
return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupLevel.PAID, expiredBackup.hashedBackupId())
|
||||
.clearMediaUsage(clock)
|
||||
.expireDirectoryNames(secureRandom, expiredBackup.expirationType())
|
||||
.addRemoveExpression(Map.entry("#mediaRefresh", ATTR_LAST_MEDIA_REFRESH))
|
||||
@@ -433,7 +433,7 @@ public class BackupsDb {
|
||||
.build())
|
||||
.thenRun(Util.NOOP);
|
||||
} else {
|
||||
return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupLevel.MEDIA, hashedBackupId)
|
||||
return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupLevel.PAID, hashedBackupId)
|
||||
.addRemoveExpression(Map.entry("#expiredPrefixes", ATTR_EXPIRED_PREFIX))
|
||||
.updateItemBuilder()
|
||||
.build())
|
||||
@@ -722,7 +722,7 @@ public class BackupsDb {
|
||||
Map.entry("#lastRefreshTime", ATTR_LAST_REFRESH),
|
||||
Map.entry(":lastRefreshTime", AttributeValues.n(refreshTime.getEpochSecond())));
|
||||
|
||||
if (backupLevel.compareTo(BackupLevel.MEDIA) >= 0) {
|
||||
if (backupLevel.compareTo(BackupLevel.PAID) >= 0) {
|
||||
// update the media time if we have the appropriate level
|
||||
addSetExpression("#lastMediaRefreshTime = :lastMediaRefreshTime",
|
||||
Map.entry("#lastMediaRefreshTime", ATTR_LAST_MEDIA_REFRESH),
|
||||
|
||||
Reference in New Issue
Block a user