diff --git a/lib/storage/file.js b/lib/storage/file.js index c9df005e8ea..6cc3b414bde 100644 --- a/lib/storage/file.js +++ b/lib/storage/file.js @@ -79,6 +79,12 @@ var STORAGE_DOWNLOAD_BASE_URL = 'https://storage.googleapis.com'; */ var STORAGE_UPLOAD_BASE_URL = 'https://www.googleapis.com/upload/storage/v1/b'; +/** + * @const {RegExp} + * @private + */ +var GS_URL_REGEXP = /^gs\:\/\/([a-z0-9_\.\-]+)\/(.+)$/; + /*! Developer Documentation * * @param {module:storage/bucket} bucket - The Bucket instance this file is @@ -284,7 +290,7 @@ nodeutil.inherits(File, ServiceObject); /** * Copy this file to another file. By default, this will copy the file to the * same bucket, but you can choose to copy it to another Bucket by providing - * either a Bucket or File object. + * a Bucket or File object or a URL starting with "gs://". * * @resource [Objects: copy API Documentation]{@link https://cloud.google.com/storage/docs/json_api/v1/objects/copy} * @@ -321,6 +327,22 @@ nodeutil.inherits(File, ServiceObject); * }); * * //- + * // If you pass in a string starting with "gs://" for the destination, the + * // file is copied to the other bucket and under the new name provided. + * //- + * var newLocation = 'gs://another-bucket/my-image-copy.png'; + * file.copy(newLocation, function(err, copiedFile, apiResponse) { + * // `my-bucket` still contains: + * // - "my-image.png" + * // + * // `another-bucket` now contains: + * // - "my-image-copy.png" + * + * // `copiedFile` is an instance of a File object that refers to your new + * // file. + * }); + * + * //- * // If you pass in a Bucket object, the file will be copied to that bucket * // using the same name. * //- @@ -366,8 +388,14 @@ File.prototype.copy = function(destination, callback) { var newFile; if (is.string(destination)) { - destBucket = this.bucket; - destName = destination; + var parsedDestination = GS_URL_REGEXP.exec(destination); + if (parsedDestination !== null && parsedDestination.length === 3) { + destBucket = this.storage.bucket(parsedDestination[1]); + destName = parsedDestination[2]; + } else { + destBucket = this.bucket; + destName = destination; + } } else if (destination.constructor && destination.constructor.name === 'Bucket') { destBucket = destination; @@ -1467,7 +1495,7 @@ File.prototype.makePublic = function(callback) { /** * Move this file to another location. By default, this will move the file to * the same bucket, but you can choose to move it to another Bucket by providing - * either a Bucket or File object. + * a Bucket or File object or a URL beginning with "gs://". * * **Warning**: * There is currently no atomic `move` method in the Google Cloud Storage API, @@ -1513,6 +1541,22 @@ File.prototype.makePublic = function(callback) { * }); * * //- + * // If you pass in a string starting with "gs://" for the destination, the + * // file is copied to the other bucket and under the new name provided. + * //- + * var newLocation = 'gs://another-bucket/my-image-new.png'; + * file.move(newLocation, function(err, destinationFile, apiResponse) { + * // `my-bucket` no longer contains: + * // - "my-image.png" + * // + * // `another-bucket` now contains: + * // - "my-image-new.png" + * + * // `destinationFile` is an instance of a File object that refers to your + * // new file. + * }); + * + * //- * // If you pass in a Bucket object, the file will be moved to that bucket * // using the same name. * //- diff --git a/system-test/storage.js b/system-test/storage.js index 9b294167f00..939f5faf7cc 100644 --- a/system-test/storage.js +++ b/system-test/storage.js @@ -849,6 +849,7 @@ describe('storage', function() { assert.ifError(err); file.copy('CloudLogoCopy', function(err, copiedFile) { + assert.ifError(err); async.parallel([ file.delete.bind(file), copiedFile.delete.bind(copiedFile) @@ -856,6 +857,34 @@ describe('storage', function() { }); }); }); + + it('should copy to another bucket given a gs:// URL', function(done) { + var opts = { destination: 'CloudLogo' }; + bucket.upload(FILES.logo.path, opts, function(err, file) { + assert.ifError(err); + + var otherBucket = storage.bucket(generateName()); + otherBucket.create(function(err) { + assert.ifError(err); + + var destPath = 'gs://' + otherBucket.name + '/CloudLogoCopy'; + file.copy(destPath, function(err) { + assert.ifError(err); + + otherBucket.getFiles(function(err, files) { + assert.ifError(err); + + assert.strictEqual(files.length, 1); + var newFile = files[0]; + + assert.strictEqual(newFile.name, 'CloudLogoCopy'); + + done(); + }); + }); + }); + }); + }); }); describe('channels', function() { diff --git a/test/storage/file.js b/test/storage/file.js index 183d46e1ec0..9cef8af4421 100644 --- a/test/storage/file.js +++ b/test/storage/file.js @@ -125,6 +125,9 @@ describe('File', function() { } else { return (requestOverride || requestCached)(req); } + }, + bucket: function(name) { + return new Bucket(this, name); } }; @@ -311,6 +314,16 @@ describe('File', function() { file.copy(newFileName); }); + it('should allow a "gs://..." string', function(done) { + var newFileName = 'gs://other-bucket/new-file-name.png'; + var expectedPath = format('/copyTo/b/{destBucket}/o/{destName}', { + destBucket: 'other-bucket', + destName: 'new-file-name.png' + }); + assertPathEquals(file, expectedPath, done); + file.copy(newFileName); + }); + it('should allow a Bucket', function(done) { var expectedPath = format('/copyTo/b/{destBucket}/o/{destName}', { destBucket: BUCKET.name,