Skip to content

Feature: Add support for restoring asset(s) by asset id #707

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,27 @@ exports.restore = function restore(public_ids, callback, options = {}) {
}, callback, options);
};

exports.restore_by_asset_ids = function restore_by_asset_ids(asset_ids, options = {}, callback) {
options.content_type = "json";
let uri = ["resources", "restore"];

// make sure asset_ids is always an array
if (!Array.isArray(asset_ids)) {
asset_ids = [asset_ids];
}

return call_api(
"post",
uri,
{
asset_ids: asset_ids,
versions: options.versions
},
callback,
options
);
};

exports.update = function update(public_id, callback, options = {}) {
let params, resource_type, type, uri;
resource_type = options.resource_type || "image";
Expand Down
1 change: 1 addition & 0 deletions lib/v2/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ v1_adapters(exports, api, {
resources_by_asset_folder: 1,
resource: 1,
restore: 1,
restore_by_asset_ids: 1,
update: 1,
delete_resources: 1,
delete_resources_by_prefix: 1,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"jsdoc": "^4.0.4",
"jsdom": "^9.12.0",
"jsdom-global": "2.1.1",
"mocha": "^6.2.3",
"mocha": "^7.2.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this intentional?

"nyc": "^14.1.1",
"rimraf": "^3.0.0",
"sinon": "^6.1.4",
Expand Down
186 changes: 186 additions & 0 deletions test/integration/api/admin/api_spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-tabs */
const sinon = require('sinon');
const formatDate = require("date-fns").format;
const subDate = require("date-fns").sub;
Expand Down Expand Up @@ -1455,6 +1456,191 @@ describe("api", function () {
expect(finalDelete.deleted[PUBLIC_ID_BACKUP_2]).to.be("deleted");
});
});

describe("restore_by_asset_ids", function () {
this.timeout(TIMEOUT.MEDIUM);

const publicId = "api_test_restore" + UNIQUE_JOB_SUFFIX_ID;
let uploadedAssetId;

before(() => uploadImage({
public_id: publicId,
backup: true,
tags: UPLOAD_TAGS
})
.then(wait(2000))
.then(() => cloudinary.v2.api.resource(publicId))
.then((resource) => {
uploadedAssetId = resource.asset_id;
expect(resource).not.to.be(null);
expect(resource.bytes).to.eql(3381);
return cloudinary.v2.api.delete_resources(publicId);
})
.then(() => cloudinary.v2.api.resource(publicId))
.then((resource) => {
expect(resource).not.to.be(null);
expect(resource.bytes).to.eql(0);
expect(resource.placeholder).to.eql(true);
})
);

it("should restore a deleted resource when passed an asset ID", () => cloudinary.v2.api
.restore_by_asset_ids([uploadedAssetId])
.then((response) => {
let info = response[uploadedAssetId];
expect(info).not.to.be(null);
expect(info.bytes).to.eql(3381);
return cloudinary.v2.api.resources_by_asset_ids([uploadedAssetId]);
})
.then((response) => {
const { resources } = response;
expect(resources[0]).not.to.be(null);
expect(resources[0].bytes).to.eql(3381);
}));

it("should restore different versions of a deleted asset when passed an asset ID", async function () {
this.timeout(TIMEOUT.LARGE);

// Upload the same file twice (upload->delete->upload->delete)

// Upload and delete a file
const firstUpload = await uploadImage({
public_id: PUBLIC_ID_BACKUP_1,
backup: true
});
await wait(1000)();

const firstDelete = await API_V2.delete_resources([
PUBLIC_ID_BACKUP_1
]);

// Upload and delete it again, this time add angle to create a different 'version'
const secondUpload = await uploadImage({
public_id: PUBLIC_ID_BACKUP_1,
backup: true,
angle: "0"
});
await wait(1000)();

const secondDelete = await API_V2.delete_resources([
PUBLIC_ID_BACKUP_1
]);
await wait(1000)();

// Sanity, ensure these uploads are different before we continue
expect(firstUpload.bytes).not.to.equal(secondUpload.bytes);

// Ensure all files were uploaded correctly
expect(firstUpload).not.to.be(null);
expect(secondUpload).not.to.be(null);

// Ensure all files were deleted correctly
expect(firstDelete).to.have.property("deleted");
expect(secondDelete).to.have.property("deleted");

// Get the asset ID and versions of the deleted asset
const getVersionsResp = await API_V2.resource(PUBLIC_ID_BACKUP_1, {
versions: true
});
const assetId = getVersionsResp.asset_id;

const firstAssetVersion = getVersionsResp.versions[0].version_id;
const secondAssetVersion = getVersionsResp.versions[1].version_id;

// Restore first version by passing in the asset ID, ensure it's equal to the upload size
await wait(1000)();
const firstVerRestore = await API_V2.restore_by_asset_ids([assetId], {
versions: [firstAssetVersion]
});

expect(firstVerRestore[assetId].bytes).to.eql(
firstUpload.bytes
);

// Restore second version by passing in the asset ID, ensure it's equal to the upload size
await wait(1000)();
const secondVerRestore = await API_V2.restore_by_asset_ids(
[assetId],
{ versions: [secondAssetVersion] }
);
expect(secondVerRestore[assetId].bytes).to.eql(
secondUpload.bytes
);

// Cleanup,
const finalDeleteResp = await API_V2.delete_resources([
PUBLIC_ID_BACKUP_1
]);
expect(finalDeleteResp).to.have.property("deleted");
});

it("should restore two different deleted assets when passed asset IDs", async () => {
// Upload two different files
const firstUpload = await uploadImage({
public_id: PUBLIC_ID_BACKUP_1,
backup: true
});
const secondUpload = await uploadImage({
public_id: PUBLIC_ID_BACKUP_2,
backup: true,
angle: "0"
});

// delete both resources
const deleteAll = await API_V2.delete_resources([
PUBLIC_ID_BACKUP_1,
PUBLIC_ID_BACKUP_2
]);

// Expect correct deletion of the assets
expect(deleteAll.deleted[PUBLIC_ID_BACKUP_1]).to.be("deleted");
expect(deleteAll.deleted[PUBLIC_ID_BACKUP_2]).to.be("deleted");

const getFirstAssetVersion = await API_V2.resource(
PUBLIC_ID_BACKUP_1,
{ versions: true }
);

const getSecondAssetVersion = await API_V2.resource(
PUBLIC_ID_BACKUP_2,
{ versions: true }
);

const firstAssetId = getFirstAssetVersion.asset_id;
const secondAssetId = getSecondAssetVersion.asset_id;

const firstAssetVersion =
getFirstAssetVersion.versions[0].version_id;
const secondAssetVersion =
getSecondAssetVersion.versions[0].version_id;

const IDS_TO_RESTORE = [firstAssetId, secondAssetId];
const VERSIONS_TO_RESTORE = [firstAssetVersion, secondAssetVersion];

const restore = await API_V2.restore_by_asset_ids(IDS_TO_RESTORE, {
versions: VERSIONS_TO_RESTORE
});

// Expect correct restorations
expect(restore[firstAssetId].bytes).to.equal(
firstUpload.bytes
);
expect(restore[secondAssetId].bytes).to.equal(
secondUpload.bytes
);

// Cleanup
const finalDelete = await API_V2.delete_resources([
PUBLIC_ID_BACKUP_1,
PUBLIC_ID_BACKUP_2
]);
// Expect correct deletion of the assets
expect(finalDelete.deleted[PUBLIC_ID_BACKUP_1]).to.be("deleted");
expect(finalDelete.deleted[PUBLIC_ID_BACKUP_2]).to.be("deleted");
});
});


describe('mapping', function () {
before(function () {
this.mapping = `api_test_upload_mapping${Math.floor(Math.random() * 100000)}`;
Expand Down
4 changes: 4 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,10 @@ declare module 'cloudinary' {

function restore(public_ids: string[], callback?: ResponseCallback): Promise<any>;

function restore_by_asset_ids(asset_ids: string[] | string, options?: AdminAndResourceOptions, callback?: ResponseCallback): Promise<ResourceApiResponse>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I see correctly, the implementation of restore_by_asset_ids doesn't support passing a single value, we're only passing the first argument as a payload when doing the request to Admin API.
Currently Admin API only supports a list of values under asset_ids which means that, either this type definition needs to change to string[] or we should wrap the first argument in array if it's not already an array, in the restore_by_asset_ids implementation.


function restore_by_asset_ids(asset_ids: string[] | string, callback?: ResponseCallback): Promise<ResourceApiResponse>;

function root_folders(callback?: ResponseCallback, options?: AdminApiOptions): Promise<any>;

function search(params: string, options?: AdminApiOptions, callback?: ResponseCallback): Promise<any>;
Expand Down