diff --git a/CHANGELOG.md b/CHANGELOG.md index b2c4fa4..92ad890 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - [#20](https://github.com/devatherock/artifactory-badge/issues/20): `/version` endpoint to generate latest version badge - `/metrics` endpoint - Enabled access logs +- [#24](https://github.com/devatherock/artifactory-badge/issues/24): Unit tests for 100% code coverage ### Changed - Caught exception when JSON processing fails @@ -21,6 +22,7 @@ - [#10](https://github.com/devatherock/artifactory-badge/issues/10): Hid decimal point when image size is a whole number - [#11](https://github.com/devatherock/artifactory-badge/issues/11): Shortened pulls count - [#25](https://github.com/devatherock/artifactory-badge/issues/25): Used a different `shields.io` URL, so that badge values with `-` are supported +- [#15](https://github.com/devatherock/artifactory-badge/issues/15): Handled HTTP 404s from artifactory APIs ### Removed - [#13](https://github.com/devatherock/artifactory-badge/issues/13): Apache HTTP Client diff --git a/build.gradle b/build.gradle index 0a24cea..242d932 100644 --- a/build.gradle +++ b/build.gradle @@ -90,15 +90,6 @@ ext.jacoco = [ exclusions: [ 'io/github/devatherock/Application.class', 'io/github/devatherock/test/ArtifactoryController.class' - ], - // Should be removed when all tests have been written - coverageThresholds: [ - 'io.github.devatherock.artifactory.service.DockerBadgeService': [ - 'BRANCH': 0.66, - 'COMPLEXITY': 0.50, - 'INSTRUCTION': 0.93, - 'LINE': 0.90 - ] ] ] apply from: '../gradle-includes/checks.gradle' \ No newline at end of file diff --git a/src/main/java/io/github/devatherock/artifactory/service/DockerBadgeService.java b/src/main/java/io/github/devatherock/artifactory/service/DockerBadgeService.java index fd54bc4..4b6833f 100644 --- a/src/main/java/io/github/devatherock/artifactory/service/DockerBadgeService.java +++ b/src/main/java/io/github/devatherock/artifactory/service/DockerBadgeService.java @@ -69,11 +69,8 @@ public String getImageSizeBadge(String packageName, String tag, String badgeLabe DockerManifest manifest = readManifest(packageName, tag); if (null != manifest && CollectionUtils.isNotEmpty(manifest.getLayers())) { - double imageSize = manifest.getLayers() - .stream() - .map(DockerLayer::getSize) - .reduce((totalSize, currentLayerSize) -> totalSize + currentLayerSize) - .get() / BYTES_IN_MB; + double imageSize = manifest.getLayers().stream().map(DockerLayer::getSize) + .reduce((totalSize, currentLayerSize) -> totalSize + currentLayerSize).get() / BYTES_IN_MB; LOGGER.info("Size of {}/{}: {} MB", packageName, tag, imageSize); return badgeGenerator.generateBadge(badgeLabel, String.format("%s MB", formatDecimal(imageSize, "0.##"))); @@ -105,13 +102,7 @@ public String getPullsCountBadge(String packageName, String badgeLabel) { for (ArtifactoryFolderElement child : folderInfo.getChildren()) { if (child.isFolder()) { - HttpRequest fileRequest = HttpRequest - .create(HttpMethod.GET, - artifactoryConfig.getStorageUrlPrefix() + packageName + child.getUri() - + FILE_NAME_MANIFEST + "?stats") - .header(HDR_API_KEY, artifactoryConfig.getApiKey()); - ArtifactoryFileStats fileStats = artifactoryClient.retrieve(fileRequest, - ArtifactoryFileStats.class); + ArtifactoryFileStats fileStats = getManifestStats(packageName, child.getUri()); if (null != fileStats) { downloadCount += fileStats.getDownloadCount(); @@ -137,9 +128,9 @@ public String getLatestVersionBadge(String packageName, String badgeLabel) { if (child.isFolder()) { ArtifactoryFolderInfo currentVersion = getArtifactoryFolderInfo(packageName + child.getUri()); - if (null == latestVersion - || Instant.from(MODIFIED_TIME_PARSER.parse(currentVersion.getLastModified())).compareTo( - Instant.from(MODIFIED_TIME_PARSER.parse(latestVersion.getLastModified()))) > 0) { + if (null == latestVersion || (null != currentVersion + && Instant.from(MODIFIED_TIME_PARSER.parse(currentVersion.getLastModified())).compareTo( + Instant.from(MODIFIED_TIME_PARSER.parse(latestVersion.getLastModified()))) > 0)) { latestVersion = currentVersion; } } @@ -163,10 +154,18 @@ public String getLatestVersionBadge(String packageName, String badgeLabel) { * @return {@link ArtifactoryFolderInfo} */ private ArtifactoryFolderInfo getArtifactoryFolderInfo(String packageName) { + ArtifactoryFolderInfo folderInfo = null; HttpRequest folderRequest = HttpRequest .create(HttpMethod.GET, artifactoryConfig.getStorageUrlPrefix() + packageName) .header(HDR_API_KEY, artifactoryConfig.getApiKey()); - return artifactoryClient.retrieve(folderRequest, ArtifactoryFolderInfo.class); + + try { + folderInfo = artifactoryClient.retrieve(folderRequest, ArtifactoryFolderInfo.class); + } catch (HttpClientResponseException exception) { + LOGGER.warn("Exception when fetching folder info of {}", packageName, exception); + } + + return folderInfo; } /** @@ -189,7 +188,6 @@ private String generateNotFoundBadge(String badgeLabel) { private DockerManifest readManifest(String packageName, String tag) { String fullPackageName = packageName + "/" + tag; DockerManifest manifest = null; - HttpRequest manifestRequest = HttpRequest .create(HttpMethod.GET, artifactoryConfig.getUrlPrefix() + fullPackageName + FILE_NAME_MANIFEST) .header(HDR_API_KEY, artifactoryConfig.getApiKey()); @@ -203,6 +201,30 @@ private DockerManifest readManifest(String packageName, String tag) { return manifest; } + /** + * Reads statistics of the {@code manifest.json} file for the supplied docker + * image and tag + * + * @param packageName the docker image name + * @param tagUri subfolder path to a docker image tag + * @return {@link ArtifactoryFileStats} + */ + private ArtifactoryFileStats getManifestStats(String packageName, String tagUri) { + ArtifactoryFileStats fileStats = null; + HttpRequest fileRequest = HttpRequest + .create(HttpMethod.GET, + artifactoryConfig.getStorageUrlPrefix() + packageName + tagUri + FILE_NAME_MANIFEST + "?stats") + .header(HDR_API_KEY, artifactoryConfig.getApiKey()); + + try { + fileStats = artifactoryClient.retrieve(fileRequest, ArtifactoryFileStats.class); + } catch (HttpClientResponseException exception) { + LOGGER.warn("Exception when reading manifest stats of {}{}", packageName, tagUri, exception); + } + + return fileStats; + } + /** * Returns the formatted value to be displayed in the version badge * diff --git a/src/test/groovy/io/github/devatherock/artifactory/controllers/DockerControllerSpec.groovy b/src/test/groovy/io/github/devatherock/artifactory/controllers/DockerControllerSpec.groovy index bbf6ea8..8dd0b5e 100644 --- a/src/test/groovy/io/github/devatherock/artifactory/controllers/DockerControllerSpec.groovy +++ b/src/test/groovy/io/github/devatherock/artifactory/controllers/DockerControllerSpec.groovy @@ -52,13 +52,13 @@ class DockerControllerSpec extends Specification { .willReturn(WireMock.okJson( TestUtil.getFoldersResponse('/devatherock/simple-slack', '2020-10-01T00:00:00.000Z')))) WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}/1.1.0/manifest.json?stats") - .willReturn(WireMock.okJson(TestUtil.getManifestStats('1.1.0', 10)))) + .willReturn(WireMock.okJson(TestUtil.getManifestStats(10)))) WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}/1.1.2/manifest.json?stats") - .willReturn(WireMock.okJson(TestUtil.getManifestStats('1.1.2', 20)))) + .willReturn(WireMock.okJson(TestUtil.getManifestStats(20)))) WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}/latest/manifest.json?stats") - .willReturn(WireMock.okJson(TestUtil.getManifestStats('latest', 30)))) + .willReturn(WireMock.okJson(TestUtil.getManifestStats(30)))) WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}/abcdefgh/manifest.json?stats") - .willReturn(WireMock.okJson(TestUtil.getManifestStats('abcdefgh', 40)))) + .willReturn(WireMock.okJson(TestUtil.getManifestStats(40)))) WireMock.givenThat(WireMock.get(WireMock.urlPathEqualTo('/static/v1')) .withQueryParam('label', equalTo('docker pulls')) .withQueryParam('message', equalTo('100')) @@ -99,13 +99,13 @@ class DockerControllerSpec extends Specification { .willReturn(WireMock.okJson( TestUtil.getFoldersResponse('/devatherock/simple-slack', '2020-10-01T00:00:00.000Z')))) WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}/1.1.0/manifest.json?stats") - .willReturn(WireMock.okJson(TestUtil.getManifestStats('1.1.0', 10)))) + .willReturn(WireMock.okJson(TestUtil.getManifestStats(10)))) WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}/1.1.2/manifest.json?stats") - .willReturn(WireMock.okJson(TestUtil.getManifestStats('1.1.2', 20)))) + .willReturn(WireMock.okJson(TestUtil.getManifestStats(20)))) WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}/latest/manifest.json?stats") - .willReturn(WireMock.okJson(TestUtil.getManifestStats('latest', 30)))) + .willReturn(WireMock.okJson(TestUtil.getManifestStats(30)))) WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}/abcdefgh/manifest.json?stats") - .willReturn(WireMock.okJson(TestUtil.getManifestStats('abcdefgh', 40)))) + .willReturn(WireMock.okJson(TestUtil.getManifestStats(40)))) WireMock.givenThat(WireMock.get(WireMock.urlPathEqualTo('/static/v1')) .withQueryParam('label', equalTo('downloads')) .withQueryParam('message', equalTo('100')) diff --git a/src/test/groovy/io/github/devatherock/artifactory/service/DockerBadgeServiceSpec.groovy b/src/test/groovy/io/github/devatherock/artifactory/service/DockerBadgeServiceSpec.groovy index 326b4e0..ba8bc0b 100644 --- a/src/test/groovy/io/github/devatherock/artifactory/service/DockerBadgeServiceSpec.groovy +++ b/src/test/groovy/io/github/devatherock/artifactory/service/DockerBadgeServiceSpec.groovy @@ -5,6 +5,7 @@ import com.github.tomakehurst.wiremock.client.WireMock import io.github.devatherock.artifactory.config.ArtifactoryProperties import io.github.devatherock.artifactory.entities.ArtifactoryFolderInfo import io.github.devatherock.artifactory.util.BadgeGenerator +import io.github.devatherock.test.TestUtil import io.micronaut.http.client.BlockingHttpClient import io.micronaut.http.client.HttpClient import spock.lang.Shared @@ -65,6 +66,251 @@ class DockerBadgeServiceSpec extends Specification { badge == 'dummyBadge' } + void 'test get image size badge - manifest without layers'() { + given: + String packageName = 'docker/devatherock/simple-slack' + + and: + WireMock.givenThat(WireMock.get("/artifactory/${packageName}/latest/manifest.json") + .willReturn(WireMock.okJson('{}'))) + + when: + String badge = dockerBadgeService.getImageSizeBadge(packageName, 'latest', 'image size') + + then: + WireMock.verify(1, + WireMock.getRequestedFor(urlEqualTo("/artifactory/${packageName}/latest/manifest.json")) + .withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey'))) + 1 * badgeGenerator.generateBadge('image size', 'Not Found') >> 'dummyBadge' + badge == 'dummyBadge' + } + + void 'test get image layers badge - manifest not found'() { + given: + String packageName = 'docker/devatherock/simple-slack' + + and: + WireMock.givenThat(WireMock.get("/artifactory/${packageName}/latest/manifest.json") + .willReturn(WireMock.notFound())) + + when: + String badge = dockerBadgeService.getImageLayersBadge(packageName, 'latest', 'layers') + + then: + WireMock.verify(1, + WireMock.getRequestedFor(urlEqualTo("/artifactory/${packageName}/latest/manifest.json")) + .withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey'))) + 1 * badgeGenerator.generateBadge('layers', 'Not Found') >> 'dummyBadge' + badge == 'dummyBadge' + } + + void 'test get image layers badge - manifest without layers'() { + given: + String packageName = 'docker/devatherock/simple-slack' + + and: + WireMock.givenThat(WireMock.get("/artifactory/${packageName}/latest/manifest.json") + .willReturn(WireMock.okJson('{}'))) + + when: + String badge = dockerBadgeService.getImageLayersBadge(packageName, 'latest', 'layers') + + then: + WireMock.verify(1, + WireMock.getRequestedFor(urlEqualTo("/artifactory/${packageName}/latest/manifest.json")) + .withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey'))) + 1 * badgeGenerator.generateBadge('layers', 'Not Found') >> 'dummyBadge' + badge == 'dummyBadge' + } + + void 'test get pulls count badge - image not found'() { + given: + String packageName = 'docker/devatherock/simple-slack' + + and: + WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}") + .willReturn(WireMock.notFound())) + + when: + String badge = dockerBadgeService.getPullsCountBadge(packageName, 'docker pulls') + + then: + WireMock.verify(1, + WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}")) + .withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey'))) + 1 * badgeGenerator.generateBadge('docker pulls', 'Not Found') >> 'dummyBadge' + badge == 'dummyBadge' + } + + void 'test get pulls count badge - only image root folder exists'() { + given: + String packageName = 'docker/devatherock/simple-slack' + + and: + WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}") + .willReturn(WireMock.okJson('{}'))) + + when: + String badge = dockerBadgeService.getPullsCountBadge(packageName, 'docker pulls') + + then: + WireMock.verify(1, + WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}")) + .withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey'))) + 1 * badgeGenerator.generateBadge('docker pulls', 'Not Found') >> 'dummyBadge' + badge == 'dummyBadge' + } + + void 'test get pulls count badge - folder is not a docker image and contains files'() { + given: + String packageName = 'io/github/devatherock/simple-yaml' + + and: + WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}") + .willReturn(WireMock.okJson(TestUtil.getFolderWithFileResponse()))) + WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}/1.1.0/manifest.json?stats") + .willReturn(WireMock.notFound())) + + when: + String badge = dockerBadgeService.getPullsCountBadge(packageName, 'docker pulls') + + then: + WireMock.verify(1, + WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}")) + .withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey'))) + WireMock.verify(1, + WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}/1.1.0/manifest.json?stats")) + .withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey'))) + WireMock.verify(0, + WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}/maven-metadata.xml/manifest.json?stats"))) + 1 * badgeGenerator.generateBadge('docker pulls', '0') >> 'dummyBadge' + badge == 'dummyBadge' + } + + void 'test get latest version badge - image not found'() { + given: + String packageName = 'docker/devatherock/simple-slack' + + and: + WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}") + .willReturn(WireMock.notFound())) + + when: + String badge = dockerBadgeService.getLatestVersionBadge(packageName, 'version') + + then: + WireMock.verify(1, + WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}")) + .withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey'))) + 1 * badgeGenerator.generateBadge('version', 'Not Found') >> 'dummyBadge' + badge == 'dummyBadge' + } + + void 'test get latest version badge - only image root folder exists'() { + given: + String packageName = 'docker/devatherock/simple-slack' + + and: + WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}") + .willReturn(WireMock.okJson('{}'))) + + when: + String badge = dockerBadgeService.getLatestVersionBadge(packageName, 'version') + + then: + WireMock.verify(1, + WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}")) + .withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey'))) + 1 * badgeGenerator.generateBadge('version', 'Not Found') >> 'dummyBadge' + badge == 'dummyBadge' + } + + void 'test get latest version badge - folder is not a docker image and contains files'() { + given: + String packageName = 'io/github/devatherock/simple-yaml' + + and: + WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}") + .willReturn(WireMock.okJson(TestUtil.getFolderWithFileResponse()))) + WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}/1.1.0") + .willReturn(WireMock.okJson(TestUtil.getFoldersResponse("${packageName}/1.1.0", '2020-10-01T00:00:00.000Z')))) + + when: + String badge = dockerBadgeService.getLatestVersionBadge(packageName, 'version') + + then: + WireMock.verify(1, + WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}")) + .withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey'))) + WireMock.verify(1, + WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}/1.1.0")) + .withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey'))) + WireMock.verify(0, + WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}/maven-metadata.xml"))) + 1 * badgeGenerator.generateBadge('version', 'v1.1.0') >> 'dummyBadge' + badge == 'dummyBadge' + } + + void 'test get latest version badge - folder is not a docker image and contains only files'() { + given: + String packageName = 'io/github/devatherock/simple-yaml' + + and: + WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}") + .willReturn(WireMock.okJson(TestUtil.getFolderWithOnlyFileResponse()))) + + when: + String badge = dockerBadgeService.getLatestVersionBadge(packageName, 'version') + + then: + WireMock.verify(1, + WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}")) + .withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey'))) + WireMock.verify(0, + WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}/maven-metadata.xml"))) + 1 * badgeGenerator.generateBadge('version', 'Not Found') >> 'dummyBadge' + badge == 'dummyBadge' + } + + void 'test get latest version badge - exception when fetching version modified time'() { + given: + String packageName = 'docker/devatherock/simple-slack' + + and: + WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}") + .willReturn(WireMock.okJson(TestUtil.getFoldersResponse(packageName, '2020-10-01T00:00:00.000Z')))) + WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}/1.1.0") + .willReturn(WireMock.okJson(TestUtil.getFoldersResponse("${packageName}/1.1.0", '2020-10-01T00:00:00.000Z')))) + WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}/1.1.2") + .willReturn(WireMock.notFound())) + WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}/latest") + .willReturn(WireMock.notFound())) + WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}/abcdefgh") + .willReturn(WireMock.notFound())) + + when: + String badge = dockerBadgeService.getLatestVersionBadge(packageName, 'version') + + then: + WireMock.verify(1, + WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}")) + .withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey'))) + WireMock.verify(1, + WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}/1.1.0")) + .withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey'))) + WireMock.verify(1, + WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}/1.1.2")) + .withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey'))) + WireMock.verify(1, + WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}/latest")) + .withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey'))) + WireMock.verify(1, + WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}/abcdefgh")) + .withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey'))) + 1 * badgeGenerator.generateBadge('version', 'v1.1.0') >> 'dummyBadge' + badge == 'dummyBadge' + } + void 'test get version badge value'() { expect: dockerBadgeService.getVersionBadgeValue(new ArtifactoryFolderInfo(path: path)) == outputVersion diff --git a/src/test/groovy/io/github/devatherock/test/TestUtil.groovy b/src/test/groovy/io/github/devatherock/test/TestUtil.groovy index 38c069e..2c27cb1 100644 --- a/src/test/groovy/io/github/devatherock/test/TestUtil.groovy +++ b/src/test/groovy/io/github/devatherock/test/TestUtil.groovy @@ -31,14 +31,52 @@ class TestUtil { "uri": "/abcdefgh", "folder": true } - ], - "uri": "http://localhost:8081/artifactory/api/storage/docker/devatherock/simple-slack" + ] + }""" + } + + static String getFolderWithFileResponse() { + """{ + "repo": "docker", + "path": "io/github/devatherock/simple-yaml", + "created": "2020-10-01T00:00:00.000Z", + "createdBy": "devatherock", + "lastModified": "2020-10-01T00:00:00.000Z", + "modifiedBy": "devatherock", + "lastUpdated": "2020-10-01T00:00:00.000Z", + "children": [ + { + "uri": "/1.1.0", + "folder": true + }, + { + "uri": "/maven-metadata.xml", + "folder": false + } + ] + }""" + } + + static String getFolderWithOnlyFileResponse() { + """{ + "repo": "docker", + "path": "io/github/devatherock/simple-yaml", + "created": "2020-10-01T00:00:00.000Z", + "createdBy": "devatherock", + "lastModified": "2020-10-01T00:00:00.000Z", + "modifiedBy": "devatherock", + "lastUpdated": "2020-10-01T00:00:00.000Z", + "children": [ + { + "uri": "/maven-metadata.xml", + "folder": false + } + ] }""" } - static String getManifestStats(String tag, int downloadCount) { + static String getManifestStats(int downloadCount) { """{ - "uri": "http://localhost:8081/artifactory/docker/devatherock/simple-slack/${tag}/manifest.json", "downloadCount": ${downloadCount}, "lastDownloaded": 1602863958001, "lastDownloadedBy": "devatherock",