Skip to content

Commit c2f0460

Browse files
authored
Added "updated" metadata to the ExportedApi (#8191)
* Added "updated" metadata to the ExportedApi * Use same pattern for metadata * updated -> validated
1 parent ff9d371 commit c2f0460

File tree

1 file changed

+86
-32
lines changed

1 file changed

+86
-32
lines changed

app/lib/package/api_export/exported_api.dart

Lines changed: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -259,21 +259,28 @@ final class ExportedJsonFile<T> extends ExportedObject {
259259
this._maxAge,
260260
) : super._(_owner, _objectName);
261261

262-
late final _metadata = ObjectMetadata(
263-
contentType: 'application/json; charset="utf-8"',
264-
contentEncoding: 'gzip',
265-
cacheControl: 'public, max-age=${_maxAge.inSeconds}',
266-
);
262+
ObjectMetadata _metadata() {
263+
return ObjectMetadata(
264+
contentType: 'application/json; charset="utf-8"',
265+
contentEncoding: 'gzip',
266+
cacheControl: 'public, max-age=${_maxAge.inSeconds}',
267+
custom: {
268+
'validated': clock.now().toIso8601String(),
269+
},
270+
);
271+
}
267272

268273
/// Write [data] as gzipped JSON in UTF-8 format.
269274
Future<void> write(T data) async {
270275
final gzipped = _jsonGzip.encode(data);
276+
final metadata = _metadata();
277+
271278
await Future.wait(_owner._prefixes.map((prefix) async {
272279
await _owner._pool.withResource(() async {
273280
await _owner._bucket.writeBytesIfDifferent(
274281
prefix + _objectName,
275282
gzipped,
276-
metadata: _metadata,
283+
metadata,
277284
);
278285
});
279286
}));
@@ -299,52 +306,88 @@ final class ExportedBlob extends ExportedObject {
299306
this._maxAge,
300307
) : super._(_owner, _objectName);
301308

302-
late final _metadata = ObjectMetadata(
303-
contentType: _contentType,
304-
cacheControl: 'public, max-age=${_maxAge.inSeconds}',
305-
contentDisposition: 'attachment; filename="$_filename"',
306-
);
309+
ObjectMetadata _metadata() {
310+
return ObjectMetadata(
311+
contentType: _contentType,
312+
cacheControl: 'public, max-age=${_maxAge.inSeconds}',
313+
contentDisposition: 'attachment; filename="$_filename"',
314+
custom: {
315+
'validated': clock.now().toIso8601String(),
316+
},
317+
);
318+
}
307319

308320
/// Write binary blob to this file.
309321
Future<void> write(List<int> data) async {
322+
final metadata = _metadata();
310323
await Future.wait(_owner._prefixes.map((prefix) async {
311324
await _owner._pool.withResource(() async {
312325
await _owner._bucket.writeBytesIfDifferent(
313326
prefix + _objectName,
314327
data,
315-
metadata: _metadata,
328+
metadata,
316329
);
317330
});
318331
}));
319332
}
320333

321-
/// Copy binary blob from [absoluteObjectName] to this file.
322-
///
323-
/// Notice that [absoluteObjectName] must be an a GCS URI including `gs://`.
324-
/// This means that it must include bucket name.
325-
/// Such URIs can be created with [Bucket.absoluteObjectName].
326-
Future<void> copyFrom(String absoluteObjectName) async {
334+
/// Copy binary blob from [bucket] and [source] to this file.
335+
Future<void> copyFrom(Bucket bucket, String source) async {
336+
final metadata = _metadata();
337+
Future<ObjectInfo?>? srcInfo;
338+
327339
await Future.wait(_owner._prefixes.map((prefix) async {
328340
await _owner._pool.withResource(() async {
341+
final dst = prefix + _objectName;
342+
343+
// Check if the dst already exists
344+
if (await _owner._bucket.tryInfo(dst) case final dstInfo?) {
345+
// Fetch info for source object (if we haven't already done this)
346+
srcInfo ??= bucket.tryInfo(source);
347+
if (await srcInfo case final srcInfo?) {
348+
if (dstInfo.contentEquals(srcInfo)) {
349+
// If both source and dst exists, and their content matches, then
350+
// we only need to update the "validated" metadata. And we only
351+
// need to update the "validated" timestamp if it's older than
352+
// _retouchDeadline
353+
final retouchDeadline = clock.agoBy(_revalidateAfter);
354+
if (dstInfo.metadata.validated.isBefore(retouchDeadline)) {
355+
await _owner._bucket.updateMetadata(dst, metadata);
356+
}
357+
return;
358+
}
359+
}
360+
}
361+
362+
// If dst or source doesn't exist, then we shall attempt to make a copy.
363+
// (if source doesn't exist we'll consistently get an error from here!)
329364
await _owner._storage.copyObject(
330-
absoluteObjectName,
331-
_owner._bucket.absoluteObjectName(prefix + _objectName),
332-
metadata: _metadata,
365+
bucket.absoluteObjectName(source),
366+
_owner._bucket.absoluteObjectName(dst),
367+
metadata: metadata,
333368
);
334369
});
335370
}));
336371
}
337372
}
338373

374+
const _revalidateAfter = Duration(days: 1);
375+
339376
extension on Bucket {
340377
Future<void> writeBytesIfDifferent(
341378
String name,
342-
List<int> bytes, {
343-
ObjectMetadata? metadata,
344-
}) async {
345-
if (await _hasSameContent(name, bytes)) {
346-
return;
379+
List<int> bytes,
380+
ObjectMetadata metadata,
381+
) async {
382+
if (await tryInfo(name) case final info?) {
383+
if (info.isSameContent(bytes)) {
384+
if (info.metadata.validated.isBefore(clock.agoBy(_revalidateAfter))) {
385+
await updateMetadata(name, metadata);
386+
}
387+
return;
388+
}
347389
}
390+
348391
await uploadWithRetry(
349392
this,
350393
name,
@@ -353,16 +396,27 @@ extension on Bucket {
353396
metadata: metadata,
354397
);
355398
}
399+
}
356400

357-
Future<bool> _hasSameContent(String name, List<int> bytes) async {
358-
final info = await tryInfo(name);
359-
if (info == null) {
401+
extension on ObjectInfo {
402+
bool isSameContent(List<int> bytes) {
403+
if (length != bytes.length) {
360404
return false;
361405
}
362-
if (info.length != bytes.length) {
406+
final bytesHash = md5.convert(bytes).bytes;
407+
return fixedTimeIntListEquals(md5Hash, bytesHash);
408+
}
409+
410+
bool contentEquals(ObjectInfo info) {
411+
if (length != info.length) {
363412
return false;
364413
}
365-
final md5Hash = md5.convert(bytes).bytes;
366-
return fixedTimeIntListEquals(info.md5Hash, md5Hash);
414+
return fixedTimeIntListEquals(md5Hash, info.md5Hash);
415+
}
416+
}
417+
418+
extension on ObjectMetadata {
419+
DateTime get validated {
420+
return DateTime.tryParse(custom?['validated'] ?? '') ?? DateTime(0);
367421
}
368422
}

0 commit comments

Comments
 (0)