copy method
Copy an object, including its metadata.
Local: filesystem-level copy. Encrypted: re-encrypts when the
destination resolves to a different encryption key. Remote: server-
side CopyObject. The metadata travels with the body.
Implementation
@override
Future<void> copy(String sourceKey, String destKey) async {
checkNotDisposed();
final info = await _inner.head(sourceKey);
if (info == null) throw FileNotFoundError(sourceKey);
if (info.metadata[_metaEncrypted] != 'true') {
// Not encrypted — simple copy
return _inner.copy(sourceKey, destKey);
}
// Encrypted — re-encrypt with the destination key
// This ensures cross-profile copies work correctly
final sourceEncKey = await _keyResolver?.resolveKey(sourceKey);
final destEncKey = await _keyResolver?.resolveKey(destKey);
if (sourceEncKey == null && destEncKey == null) {
// Locked copy — neither side resolves (e.g. a locked profile).
// A byte copy keeps the object intact and still encrypted with its
// original key: nothing is exposed, nothing is mis-keyed.
return _inner.copy(sourceKey, destKey);
}
if (sourceEncKey == null || destEncKey == null) {
// Exactly one side resolves. A byte copy here would stamp the
// destination with ciphertext its own key can't decrypt (MAC
// failures later, far from the cause); re-keying silently is worse.
throw EncryptionKeyMissingError(
sourceEncKey == null ? sourceKey : destKey,
);
}
// Read-decrypt source → write-encrypt dest, carrying the user metadata
// across. writeStream re-derives the internal _metaEncrypted/_metaOriginalSize
// flags for the destination, so passing them straight through is harmless.
final plainStream = readStream(sourceKey);
await writeStream(
destKey,
plainStream,
WriteOptions(
encrypt: true,
encryptionKey: destEncKey,
contentType: info.contentType,
metadata: info.metadata,
),
);
}