@@ -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+
339376extension 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