From 7b35c0c0d586565e98ce80653da43aedec536c3e Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Sun, 5 Mar 2017 20:47:23 -0800 Subject: [PATCH 01/14] refactored BuildService --- static/js/quay.module.ts | 2 + static/js/services/build-service.js | 62 -------------- .../services/build/build.service.impl.spec.ts | 76 +++++++++++++++++ .../js/services/build/build.service.impl.ts | 84 +++++++++++++++++++ static/js/services/build/build.service.ts | 19 +++++ static/js/services/view-array/view-array.ts | 2 + 6 files changed, 183 insertions(+), 62 deletions(-) delete mode 100644 static/js/services/build-service.js create mode 100644 static/js/services/build/build.service.impl.spec.ts create mode 100644 static/js/services/build/build.service.impl.ts create mode 100644 static/js/services/build/build.service.ts diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index f8d54c5a0..b0fb55d44 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -12,6 +12,7 @@ import { LinearWorkflowComponent } from './directives/ui/linear-workflow/linear- import { LinearWorkflowSectionComponent } from './directives/ui/linear-workflow/linear-workflow-section.component'; import { QuayConfig } from './quay-config.module'; import { QuayRun } from './quay-run.module'; +import { BuildServiceImpl } from './services/build/build.service.impl'; /** @@ -33,6 +34,7 @@ import { QuayRun } from './quay-run.module'; ], providers: [ ViewArrayImpl, + BuildServiceImpl, ], }) export class quay { diff --git a/static/js/services/build-service.js b/static/js/services/build-service.js deleted file mode 100644 index 0f68edec2..000000000 --- a/static/js/services/build-service.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Service which provides helper methods for reasoning about builds. - */ -angular.module('quay').factory('BuildService', [function() { - var buildService = {}; - buildService.isActive = function(build) { - return build.phase != 'complete' && build.phase != 'error' && build.phase != 'expired' && build.phase != 'cancelled'; - }; - - buildService.getBuildMessage = function(phase) { - switch (phase) { - case 'cannot_load': - return 'Cannot load build status'; - - case 'starting': - case 'initializing': - return 'Starting Dockerfile build'; - - case 'waiting': - return 'Waiting for available build worker'; - - case 'unpacking': - return 'Unpacking build package'; - - case 'pulling': - return 'Pulling base image'; - - case 'building': - return 'Building image from Dockerfile'; - - case 'checking-cache': - return 'Looking up cached images'; - - case 'priming-cache': - return 'Priming cache for build'; - - case 'build-scheduled': - return 'Preparing build node'; - - case 'pushing': - return 'Pushing image built from Dockerfile'; - - case 'complete': - return 'Dockerfile build completed and pushed'; - - case 'error': - return 'Dockerfile build failed'; - - case 'expired': - return 'Build did not complete after 3 attempts. Re-submit this build to try again.'; - - case 'internalerror': - return 'An internal system error occurred while building; the build will be retried in the next few minutes.'; - - case 'cancelled': - return 'This build was previously cancelled.'; - - } - }; - - return buildService; -}]); diff --git a/static/js/services/build/build.service.impl.spec.ts b/static/js/services/build/build.service.impl.spec.ts new file mode 100644 index 000000000..48c5aa841 --- /dev/null +++ b/static/js/services/build/build.service.impl.spec.ts @@ -0,0 +1,76 @@ +import { BuildServiceImpl } from './build.service.impl'; + + +describe("BuildServiceImpl", () => { + var buildServiceImpl: BuildServiceImpl; + var build: {phase: string}; + + beforeEach(() => { + buildServiceImpl = new BuildServiceImpl(); + build = {phase: ""}; + }); + + describe("isActive", () => { + var phases: string[]; + + beforeEach(() => { + phases = ['complete', 'error', 'expired', 'cancelled']; + }); + + it("returns false if given build's phase matches an inactive phase", () => { + phases.forEach((phase: string) => { + build.phase = phase; + + expect(buildServiceImpl.isActive(build)).toBe(false); + }); + }); + + it("returns true if given build's phase does not match inactive phases", () => { + build.phase = 'initializing'; + + expect(buildServiceImpl.isActive(build)).toBe(true); + }); + }); + + describe("getBuildMessage", () => { + var buildMessages: {phase: string, message: string}[]; + + beforeEach(() => { + buildMessages = [ + {phase: 'cannot_load', message: 'Cannot load build status'}, + {phase: 'starting', message: 'Starting Dockerfile build'}, + {phase: 'initializing', message: 'Starting Dockerfile build'}, + {phase: 'waiting', message: 'Waiting for available build worker'}, + {phase: 'unpacking', message: 'Unpacking build package'}, + {phase: 'pulling', message: 'Pulling base image'}, + {phase: 'building', message: 'Building image from Dockerfile'}, + {phase: 'checking-cache', message: 'Looking up cached images'}, + {phase: 'priming-cache', message: 'Priming cache for build'}, + {phase: 'build-scheduled', message: 'Preparing build node'}, + {phase: 'pushing', message: 'Pushing image built from Dockerfile'}, + {phase: 'complete', message: 'Dockerfile build completed and pushed'}, + {phase: 'error', message: 'Dockerfile build failed'}, + {phase: 'expired', message: 'Build did not complete after 3 attempts. Re-submit this build to try again.'}, + {phase: 'internalerror', message: 'An internal system error occurred while building; the build will be retried in the next few minutes.'}, + {phase: 'cancelled', message: 'This build was previously cancelled.'}, + ]; + }); + + it("returns the correct message for the given phase", () => { + buildMessages.forEach((buildMessage) => { + expect(buildServiceImpl.getBuildMessage(buildMessage.phase)).toEqual(buildMessage.message, buildMessage); + }); + }); + + it("throws an error if given phase is not supported", () => { + var phase: string = "not-a-phase"; + + try { + buildServiceImpl.getBuildMessage(phase); + fail("Should throw error"); + } catch (error) { + expect(error.message).toEqual("Invalid build phase"); + } + }); + }); +}); \ No newline at end of file diff --git a/static/js/services/build/build.service.impl.ts b/static/js/services/build/build.service.impl.ts new file mode 100644 index 000000000..05d08dd62 --- /dev/null +++ b/static/js/services/build/build.service.impl.ts @@ -0,0 +1,84 @@ +import { BuildService } from './build.service'; +import { Injectable } from 'angular-ts-decorators'; + + +@Injectable(BuildService.name) +export class BuildServiceImpl implements BuildService { + + private inactivePhases: string[] = ['complete', 'error', 'expired', 'cancelled']; + + public isActive(build: {phase: string}): boolean { + return this.inactivePhases.indexOf(build.phase) == -1; + } + + public getBuildMessage(phase: string): string { + var message: string; + switch (phase) { + case 'cannot_load': + message = 'Cannot load build status'; + break; + + case 'starting': + case 'initializing': + message = 'Starting Dockerfile build'; + break; + + case 'waiting': + message = 'Waiting for available build worker'; + break; + + case 'unpacking': + message = 'Unpacking build package'; + break; + + case 'pulling': + message = 'Pulling base image'; + break; + + case 'building': + message = 'Building image from Dockerfile'; + break; + + case 'checking-cache': + message = 'Looking up cached images'; + break; + + case 'priming-cache': + message = 'Priming cache for build'; + break; + + case 'build-scheduled': + message = 'Preparing build node'; + break; + + case 'pushing': + message = 'Pushing image built from Dockerfile'; + break; + + case 'complete': + message = 'Dockerfile build completed and pushed'; + break; + + case 'error': + message = 'Dockerfile build failed'; + break; + + case 'expired': + message = 'Build did not complete after 3 attempts. Re-submit this build to try again.'; + break; + + case 'internalerror': + message = 'An internal system error occurred while building; the build will be retried in the next few minutes.'; + break; + + case 'cancelled': + message = 'This build was previously cancelled.'; + break; + + default: + throw new Error("Invalid build phase"); + } + + return message; + } +} \ No newline at end of file diff --git a/static/js/services/build/build.service.ts b/static/js/services/build/build.service.ts new file mode 100644 index 000000000..d07f9d111 --- /dev/null +++ b/static/js/services/build/build.service.ts @@ -0,0 +1,19 @@ +/** + * Service which provides helper methods for reasoning about builds. + */ +export abstract class BuildService { + + /** + * Determine if the given build is active. + * @param build The build object. + * @return isActive If the given build is active. + */ + public abstract isActive(build: {phase: string}): boolean; + + /** + * Generate a message based on a given phase. + * @param phase The phase type. + * @return buildMessage The message associated with the given phase. + */ + public abstract getBuildMessage(phase: string): string; +} \ No newline at end of file diff --git a/static/js/services/view-array/view-array.ts b/static/js/services/view-array/view-array.ts index fbdfaf4db..4b7abbd35 100644 --- a/static/js/services/view-array/view-array.ts +++ b/static/js/services/view-array/view-array.ts @@ -1,4 +1,6 @@ import { ViewArrayImpl } from "static/js/services/view-array/view-array.impl"; + + /** * Specialized wrapper around array which provides a toggle() method for viewing the contents of the * array in a manner that is asynchronously filled in over a short time period. This prevents long From ff6673fb0750cd569c58999fba4b15a84cd515f7 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Mon, 6 Mar 2017 00:02:57 -0800 Subject: [PATCH 02/14] refactored AvatarService --- karma.conf.js | 6 +- package.json | 1 + static/js/quay.module.ts | 2 + static/js/services/avatar-service.js | 49 ---------- .../avatar/avatar.service.impl.spec.ts | 97 +++++++++++++++++++ .../js/services/avatar/avatar.service.impl.ts | 49 ++++++++++ static/js/services/avatar/avatar.service.ts | 22 +++++ .../services/page/page.service.impl.spec.ts | 6 +- 8 files changed, 177 insertions(+), 55 deletions(-) delete mode 100644 static/js/services/avatar-service.js create mode 100644 static/js/services/avatar/avatar.service.impl.spec.ts create mode 100644 static/js/services/avatar/avatar.service.impl.ts create mode 100644 static/js/services/avatar/avatar.service.ts diff --git a/karma.conf.js b/karma.conf.js index 98114ba2f..841933cd6 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -27,7 +27,7 @@ module.exports = function(config) { // static/lib resources 'static/lib/**/*.js', - // Application resources + // Tests 'static/js/**/*.spec.ts*', // Tests utils @@ -37,13 +37,13 @@ module.exports = function(config) { preprocessors: { 'static/lib/ngReact/react.ngReact.min.js': ['webpack'], 'static/lib/angular-moment.min.js': ['webpack'], - 'static/js/**/*.ts*': ['webpack'], + 'static/js/**/*.spec.ts*': ['webpack'], }, webpack: webpackConfig, webpackMiddleware: { stats: 'errors-only' }, - reporters: ['dots', 'coverage', 'karma-typescript'], + reporters: ['dots', 'coverage'], coverageReporter: { dir: 'coverage', type: 'html' diff --git a/package.json b/package.json index dbc5c0019..a0da58391 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@types/angular": "1.5.16", "@types/angular-mocks": "^1.5.8", "@types/angular-route": "^1.3.3", + "@types/angular-sanitize": "^1.3.4", "@types/es6-shim": "^0.31.32", "@types/jasmine": "^2.5.41", "@types/react": "0.14.39", diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index b0fb55d44..a21188a25 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -13,6 +13,7 @@ import { LinearWorkflowSectionComponent } from './directives/ui/linear-workflow/ import { QuayConfig } from './quay-config.module'; import { QuayRun } from './quay-run.module'; import { BuildServiceImpl } from './services/build/build.service.impl'; +import { AvatarServiceImpl } from './services/avatar/avatar.service.impl'; /** @@ -35,6 +36,7 @@ import { BuildServiceImpl } from './services/build/build.service.impl'; providers: [ ViewArrayImpl, BuildServiceImpl, + AvatarServiceImpl, ], }) export class quay { diff --git a/static/js/services/avatar-service.js b/static/js/services/avatar-service.js deleted file mode 100644 index 0b1503bfe..000000000 --- a/static/js/services/avatar-service.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Service which provides helper methods for retrieving the avatars displayed in the app. - */ -angular.module('quay').factory('AvatarService', ['Config', '$sanitize', 'md5', - function(Config, $sanitize, md5) { - var avatarService = {}; - var cache = {}; - - avatarService.getAvatar = function(hash, opt_size, opt_notfound) { - var size = opt_size || 16; - switch (Config['AVATAR_KIND']) { - case 'local': - return '/avatar/' + hash + '?size=' + size; - break; - - case 'gravatar': - var notfound = opt_notfound || '404'; - return '//www.gravatar.com/avatar/' + hash + '?d=' + notfound + '&size=' + size; - break; - } - }; - - avatarService.computeHash = function(opt_email, opt_name) { - var email = opt_email || ''; - var name = opt_name || ''; - - var cacheKey = email + ':' + name; - if (!cacheKey) { return '-'; } - - if (cache[cacheKey]) { - return cache[cacheKey]; - } - - var hash = md5.createHash(email.toString().toLowerCase()); - switch (Config['AVATAR_KIND']) { - case 'local': - if (name) { - hash = name[0] + hash; - } else if (email) { - hash = email[0] + hash; - } - break; - } - - return cache[cacheKey] = hash; - }; - - return avatarService; -}]); \ No newline at end of file diff --git a/static/js/services/avatar/avatar.service.impl.spec.ts b/static/js/services/avatar/avatar.service.impl.spec.ts new file mode 100644 index 000000000..060fa8b7d --- /dev/null +++ b/static/js/services/avatar/avatar.service.impl.spec.ts @@ -0,0 +1,97 @@ +import { AvatarServiceImpl } from './avatar.service.impl'; + + +describe("AvatarServiceImpl", () => { + var avatarServiceImpl: AvatarServiceImpl; + var configMock: any; + var md5Mock: any; + + beforeEach(() => { + configMock = {AVATAR_KIND: 'local'}; + md5Mock = jasmine.createSpyObj('md5Mock', ['createHash']); + avatarServiceImpl = new AvatarServiceImpl(configMock, md5Mock); + }); + + describe("getAvatar", () => { + var hash: string; + + beforeEach(() => { + hash = "a1b2c3d4e5f6"; + }); + + it("returns a local avatar URL if given config has avatar kind set to local", () => { + var avatarURL: string = avatarServiceImpl.getAvatar(hash); + + expect(avatarURL).toEqual(`/avatar/${hash}?size=16`); + }); + + it("returns a Gravatar URL if given config has avatar kind set to Gravatar", () => { + configMock['AVATAR_KIND'] = 'gravatar'; + var avatarURL: string = avatarServiceImpl.getAvatar(hash); + + expect(avatarURL).toEqual(`//www.gravatar.com/avatar/${hash}?d=404&size=16`); + }); + + it("uses 16 as default size query parameter if not provided", () => { + var size: number = 16; + var avatarURL: string = avatarServiceImpl.getAvatar(hash); + + expect(avatarURL).toEqual(`/avatar/${hash}?size=${size}`); + }); + + it("uses 404 as default not found query parameter for Gravatar URL if not provided", () => { + configMock['AVATAR_KIND'] = 'gravatar'; + var notFound: string = '404'; + var avatarURL: string = avatarServiceImpl.getAvatar(hash); + + expect(avatarURL).toEqual(`//www.gravatar.com/avatar/${hash}?d=${notFound}&size=16`); + }); + }); + + describe("computeHash", () => { + var email: string; + var name: string; + var expectedHash: string; + + beforeEach(() => { + email = "some_example@gmail.com"; + name = "example"; + expectedHash = "a1b2c3d4e5f6"; + md5Mock.createHash = jasmine.createSpy('createHashSpy').and.returnValue(expectedHash); + }); + + it("returns hash from cache if it exists", () => { + // Call once to set the cache + avatarServiceImpl.computeHash(email, name); + md5Mock.createHash.calls.reset(); + avatarServiceImpl.computeHash(email, name); + + expect(md5Mock.createHash).not.toHaveBeenCalled(); + }); + + it("calls MD5 service to create hash using given email if cache is not set", () => { + avatarServiceImpl.computeHash(email, name); + + expect(md5Mock.createHash.calls.argsFor(0)[0]).toEqual(email.toString().toLowerCase()); + }); + + it("adds first character of given name to hash if config has avatar kind set to local", () => { + var hash: string = avatarServiceImpl.computeHash(email, name); + + expect(hash[0]).toEqual(name[0]); + }); + + it("adds first character of given email to hash if config has avatar kind set to local and not given name", () => { + var hash: string = avatarServiceImpl.computeHash(email); + + expect(hash[0]).toEqual(email[0]); + }); + + it("adds nothing to hash if config avatar kind is not set to local", () => { + configMock['AVATAR_KIND'] = 'gravatar'; + var hash: string = avatarServiceImpl.computeHash(email); + + expect(hash).toEqual(expectedHash); + }); + }); +}); \ No newline at end of file diff --git a/static/js/services/avatar/avatar.service.impl.ts b/static/js/services/avatar/avatar.service.impl.ts new file mode 100644 index 000000000..673cec473 --- /dev/null +++ b/static/js/services/avatar/avatar.service.impl.ts @@ -0,0 +1,49 @@ +import { AvatarService } from './avatar.service'; +import { Injectable } from 'angular-ts-decorators'; + + +@Injectable(AvatarService.name) +export class AvatarServiceImpl implements AvatarService { + + private cache: {[cacheKey: string]: string} = {}; + + constructor(private Config: any, private md5: any) { + + } + + public getAvatar(hash: string, size: number = 16, notFound: string = '404'): string { + var avatarURL: string; + switch (this.Config['AVATAR_KIND']) { + case 'local': + avatarURL = `/avatar/${hash}?size=${size}`; + break; + + case 'gravatar': + avatarURL = `//www.gravatar.com/avatar/${hash}?d=${notFound}&size=${size}`; + break; + } + + return avatarURL; + } + + public computeHash(email: string = '', name: string = ''): string { + const cacheKey: string = email + ':' + name; + + if (this.cache[cacheKey]) { + return this.cache[cacheKey]; + } + + var hash: string = this.md5.createHash(email.toString().toLowerCase()); + switch (this.Config['AVATAR_KIND']) { + case 'local': + if (name) { + hash = name[0] + hash; + } else if (email) { + hash = email[0] + hash; + } + break; + } + + return this.cache[cacheKey] = hash; + } +} \ No newline at end of file diff --git a/static/js/services/avatar/avatar.service.ts b/static/js/services/avatar/avatar.service.ts new file mode 100644 index 000000000..ec817e9fd --- /dev/null +++ b/static/js/services/avatar/avatar.service.ts @@ -0,0 +1,22 @@ +/** + * Service which provides helper methods for retrieving the avatars displayed in the app. + */ +export abstract class AvatarService { + + /** + * Retrieve URL for avatar image with given hash. + * @param hash Avatar image hash. + * @param size Avatar image size. + * @param notFound URL parameter if avatar image is not found. + * @return avatarURL The URL for the avatar image. + */ + public abstract getAvatar(hash: string, size?: number, notFound?: string): string; + + /** + * Compute the avatar image hash. + * @param email Email for avatar user. + * @param name Username for avatar user. + * @return hash The hash for the avatar image. + */ + public abstract computeHash(email?: string, name?: string): string; +} \ No newline at end of file diff --git a/static/js/services/page/page.service.impl.spec.ts b/static/js/services/page/page.service.impl.spec.ts index 61f9e51f1..efce5c752 100644 --- a/static/js/services/page/page.service.impl.spec.ts +++ b/static/js/services/page/page.service.impl.spec.ts @@ -9,14 +9,14 @@ describe("Service: PageServiceImpl", () => { }); describe("create", () => { - + // TODO }); describe("get", () => { - + // TODO }); describe("$get", () => { - + // TODO }); }); \ No newline at end of file From 80b3666eb751b7fc7ebb58d419f71ff384911c2b Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Tue, 7 Mar 2017 11:25:18 -0800 Subject: [PATCH 03/14] refactoring DockerfileService --- package.json | 2 +- static/js/quay.module.ts | 2 + static/js/services/dockerfile-service.js | 2 +- .../dockerfile.service.impl.spec.ts | 91 ++++++ .../dockerfile/dockerfile.service.impl.ts | 147 +++++++++ .../services/dockerfile/dockerfile.service.ts | 43 +++ test/data/test.db | Bin 1323008 -> 1323008 bytes yarn.lock | 302 +++--------------- 8 files changed, 326 insertions(+), 263 deletions(-) create mode 100644 static/js/services/dockerfile/dockerfile.service.impl.spec.ts create mode 100644 static/js/services/dockerfile/dockerfile.service.impl.ts create mode 100644 static/js/services/dockerfile/dockerfile.service.ts diff --git a/package.json b/package.json index a0da58391..f509e7e51 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "test": "./node_modules/.bin/karma start --single-run --browsers PhantomJS", "test:node": "JASMINE_CONFIG_PATH=static/test/jasmine.json ./node_modules/.bin/jasmine-ts './static/js/**/*.spec.ts'", - "build": "./node_modules/.bin/webpack --progress -p -v", + "build": "./node_modules/.bin/webpack --progress", "watch": "./node_modules/.bin/webpack --watch" }, "repository": { diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index a21188a25..a8b946fd1 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -14,6 +14,7 @@ import { QuayConfig } from './quay-config.module'; import { QuayRun } from './quay-run.module'; import { BuildServiceImpl } from './services/build/build.service.impl'; import { AvatarServiceImpl } from './services/avatar/avatar.service.impl'; +import { DockerfileServiceImpl } from './services/dockerfile/dockerfile.service.impl'; /** @@ -37,6 +38,7 @@ import { AvatarServiceImpl } from './services/avatar/avatar.service.impl'; ViewArrayImpl, BuildServiceImpl, AvatarServiceImpl, + DockerfileServiceImpl, ], }) export class quay { diff --git a/static/js/services/dockerfile-service.js b/static/js/services/dockerfile-service.js index 8763b8a13..4495d6917 100644 --- a/static/js/services/dockerfile-service.js +++ b/static/js/services/dockerfile-service.js @@ -2,7 +2,7 @@ * Service which provides helper methods for extracting information out from a Dockerfile * or an archive containing a Dockerfile. */ -angular.module('quay').factory('DockerfileService', ['DataFileService', 'Config', function(DataFileService, Config) { +angular.module('quay').factory('DockerfileServiceOld', ['DataFileService', 'Config', function(DataFileService, Config) { var dockerfileService = {}; function DockerfileInfo(contents) { diff --git a/static/js/services/dockerfile/dockerfile.service.impl.spec.ts b/static/js/services/dockerfile/dockerfile.service.impl.spec.ts new file mode 100644 index 000000000..041f31e64 --- /dev/null +++ b/static/js/services/dockerfile/dockerfile.service.impl.spec.ts @@ -0,0 +1,91 @@ +import { DockerfileServiceImpl, DockerfileInfoImpl } from './dockerfile.service.impl'; +import Spy = jasmine.Spy; + + +describe("DockerfileInfoImpl", () => { + var dockerfileInfoImpl: DockerfileInfoImpl; + var contents: string; + var configMock: any; + + beforeEach(() => { + contents = ""; + configMock = jasmine.createSpyObj('configMock', ['getDomain']); + dockerfileInfoImpl = new DockerfileInfoImpl(contents, configMock); + }); + + describe("forData", () => { + + it("returns null if given contents do not contain a 'FROM' command", () => { + expect(DockerfileInfoImpl.forData(contents, configMock)).toBe(null); + }); + + it("returns a new DockerfileInfoImpl instance if given contents are valid", () => { + contents = "FROM quay.io/coreos/nginx"; + + expect(DockerfileInfoImpl.forData(contents, configMock) instanceof DockerfileInfoImpl).toBe(true); + }); + }); + + describe("getRegistryBaseImage", () => { + + + }); + + describe("getBaseImage", () => { + var baseImageAndTag + + it("returns null if instance's contents do not contain a 'FROM' command", () => { + var getBaseImageAndTagSpy: Spy = spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue() + }); + + it("returns") + }); + + describe("getBaseImageAndTag", () => { + + it("returns null if instance's contents do not contain a 'FROM' command", () => { + expect(dockerfileInfoImpl.getBaseImageAndTag()).toBe(null); + }); + + it("returns a string containing the base image and tag from the instance's contents", () => { + contents = "FROM quay.io/coreos/nginx"; + dockerfileInfoImpl = new DockerfileInfoImpl(contents, configMock); + var baseImageAndTag: string = dockerfileInfoImpl.getBaseImageAndTag(); + + expect(baseImageAndTag).toEqual(contents.substring('FROM '.length, contents.length).trim()); + }); + + it("handles the presence of newlines", () => { + contents = "FROM quay.io/coreos/nginx\nRUN echo $0"; + dockerfileInfoImpl = new DockerfileInfoImpl(contents, configMock); + var baseImageAndTag: string = dockerfileInfoImpl.getBaseImageAndTag(); + + expect(baseImageAndTag).toEqual(contents.substring('FROM '.length, contents.indexOf('\n')).trim()); + }); + }); +}); + + +describe("DockerfileServiceImpl", () => { + var dockerfileServiceImpl: DockerfileServiceImpl; + var dataFileServiceMock: any; + var configMock: any; + + beforeEach(() => { + dataFileServiceMock = jasmine.createSpyObj('dataFileServiceMock', [ + 'readDataArrayAsPossibleArchive', + 'arrayToString', + 'blobToString', + ]); + configMock = jasmine.createSpyObj('configMock', ['getDomain']); + dockerfileServiceImpl = new DockerfileServiceImpl(dataFileServiceMock, configMock); + }); + + describe("getDockerfile", () => { + + }); + + describe("extractDockerfile", () => { + + }); +}); \ No newline at end of file diff --git a/static/js/services/dockerfile/dockerfile.service.impl.ts b/static/js/services/dockerfile/dockerfile.service.impl.ts new file mode 100644 index 000000000..fbeb02c71 --- /dev/null +++ b/static/js/services/dockerfile/dockerfile.service.impl.ts @@ -0,0 +1,147 @@ +import { DockerfileService, DockerfileInfo } from './dockerfile.service'; +import { Injectable } from 'angular-ts-decorators'; + + +@Injectable(DockerfileService.name) +export class DockerfileServiceImpl implements DockerfileService { + + constructor(private DataFileService: any, private Config: any) { + console.log(`=================== DockerfileServiceImpl ==========================`); + } + + public getDockerfile(file: any, + success: (dockerfile: DockerfileInfoImpl) => void, + failure: (error: Event | string) => void): void { + var reader: FileReader = new FileReader(); + reader.onload = (event: Event) => { + var dataArray: any = reader.result; + this.DataFileService.readDataArrayAsPossibleArchive(dataArray, (files) => { + this.processFiles(files, dataArray, success, failure); + }, () => { + // Not an archive. Read directly as a single file. + this.processFiles([], dataArray, success, failure); + }); + }; + + reader.onerror = failure; + reader.readAsArrayBuffer(file); + } + + public extractDockerfile(file: any): Promise { + return new Promise((resolve, reject) => { + // TODO: Replace callbacks with promise + }); + } + + private processFiles(files: any, + dataArray: any[], + success: (dockerfile: DockerfileInfoImpl) => void, + failure: (error: ErrorEvent | string) => void): void { + // The files array will be empty if the submitted file was not an archive. We therefore + // treat it as a single Dockerfile. + if (files.length == 0) { + this.DataFileService.arrayToString(dataArray, (contents: string) => { + var result = DockerfileInfoImpl.forData(contents, Object.assign({}, this.Config)); + if (!result) { + failure('File chosen is not a valid Dockerfile'); + return; + } + + success(result); + }); + return; + } + + var found: boolean = false; + files.forEach((file) => { + if (file['name'] == 'Dockerfile') { + this.DataFileService.blobToString(file.toBlob(), (contents: string) => { + var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, Object.assign({}, this.Config)); + if (result != null) { + failure('Dockerfile inside archive is not a valid Dockerfile'); + return; + } + + success(result); + }); + found = true; + } + }); + + if (!found) { + failure('No Dockerfile found in root of archive'); + } + } +} + + +export class DockerfileInfoImpl implements DockerfileInfo { + + constructor(private contents: string, private config: any) { + + } + + public static forData(contents: string, config: any): DockerfileInfoImpl | null { + var dockerfileInfo: DockerfileInfoImpl = null; + if (contents.indexOf('FROM ') != -1) { + dockerfileInfo = new DockerfileInfoImpl(contents, config); + } + + return dockerfileInfo; + } + + public getRegistryBaseImage(): string | null { + var baseImage = this.getBaseImage(); + if (!baseImage) { + return null; + } + + if (baseImage.indexOf(this.config.getDomain() + '/') != 0) { + return null; + } + + return baseImage.substring(this.config.getDomain().length + 1); + } + + public getBaseImage(): string | null { + var imageAndTag = this.getBaseImageAndTag(); + if (!imageAndTag) { + return null; + } + + // Note, we have to handle a few different cases here: + // 1) someimage + // 2) someimage:tag + // 3) host:port/someimage + // 4) host:port/someimage:tag + var lastIndex = imageAndTag.lastIndexOf(':'); + if (lastIndex < 0) { + return imageAndTag; + } + + // Otherwise, check if there is a / in the portion after the split point. If so, + // then the latter is part of the path (and not a tag). + var afterColon = imageAndTag.substring(lastIndex + 1); + if (afterColon.indexOf('/') >= 0) { + return imageAndTag; + } + + return imageAndTag.substring(0, lastIndex); + } + + public getBaseImageAndTag(): string | null { + var baseImageAndTag: string = null; + + const fromIndex: number = this.contents.indexOf('FROM '); + if (fromIndex != -1) { + var newlineIndex: number = this.contents.indexOf('\n', fromIndex); + if (newlineIndex == -1) { + newlineIndex = this.contents.length; + } + + baseImageAndTag = this.contents.substring(fromIndex + 'FROM '.length, newlineIndex).trim(); + } + + return baseImageAndTag; + } +} \ No newline at end of file diff --git a/static/js/services/dockerfile/dockerfile.service.ts b/static/js/services/dockerfile/dockerfile.service.ts new file mode 100644 index 000000000..db62913c2 --- /dev/null +++ b/static/js/services/dockerfile/dockerfile.service.ts @@ -0,0 +1,43 @@ +/** + * Service which provides helper methods for extracting information out from a Dockerfile + * or an archive containing a Dockerfile. + */ +export abstract class DockerfileService { + + /** + * Retrieve Dockerfile from given archive file. + * @param file File containing Dockerfile. + * @param success Success callback with retrieved Dockerfile as parameter. + * @param failure Failure callback with failure message as parameter. + */ + public abstract getDockerfile(file: any, + success: (dockerfile: DockerfileInfo) => void, + failure: (error: ErrorEvent | string) => void): void; + + public abstract extractDockerfile(file: any): Promise; +} + + +/** + * Model representing information about a specific Dockerfile. + */ +export abstract class DockerfileInfo { + + /** + * Extract the registry base image from the Dockerfile contents. + * @return registryBaseImage The registry base image. + */ + public abstract getRegistryBaseImage(): string | null; + + /** + * Extract the base image from the Dockerfile contents. + * @return baseImage The base image. + */ + public abstract getBaseImage(): string | null; + + /** + * Extract the base image and tag from the Dockerfile contents. + * @return baseImageAndTag The base image and tag. + */ + public abstract getBaseImageAndTag(): string | null; +} \ No newline at end of file diff --git a/test/data/test.db b/test/data/test.db index dc4f5960a4ca8e6d01970518b5266faa231d986f..c583ad3a65f0c25774707134c40a5fe5705d0dfb 100644 GIT binary patch delta 1324 zcmbu9-D@0G7{<@X?8ofcop-k`77T56R~FLBBs05bc6TD^Y$MiEBnzb|QE7KRw$!$E zB^8CbcK4#hiv}XOIKhGxMEnC9okHr3Exis%=}jROyoyLd(W?aYjN1lajI6u8s}-t3LkDCpUnWz!Hfp8KW^K z1+h6nm1MaTqZpTqqB%3BjU3^GaIh7+M<5co$tEPAh!IcYS7gx1myyB7-n5Js=84t7{;VwPgd>mdc9#A)za>=InHZU zUT+i+Qt{V0av(hVF8P5<``aRMnjBKtLv}kfPLt1>*gi*?wX5~0qV5*G;~mP@q#{l4 zibR*#3j2lK4V^CO{~yz6JwJx`J|(Y2%Uu?UK6y!D_t|n->66dc8wCY7G1+57o>j23 z>|uex=gCO8{~Gx%%IS*4LX@+}x?yF3OtLpiF`2VhP`UKXlBZGS;**jn?1i}p*NKEm z6iG}Hmn0-fi7H8j53Vzvs~DCLENNeA;fLK^27g^}G(1?$IX$to#?GfOtRPr{37C*q zIa#lPXryTy9sbw%f1*ZdC)n+K_7C1w05W+j9MmXSE^=N>m*+o-i|zwKY{ ziTTm|?Dtt3&n7nz{2}*BZpRJ!^YAfZ7XCh+-dFf0p2^O|`)Nr=qDep!&E+Kf!g&9= zKdI)}cYVltNfladUKVw7kcK6|2d-BV3X%AMOpZ z#hs?<-I|`>tf*-$<$W}J>EZ?b%-KugYAj#Jz3?&4Yg$^KSoGoOZ#X>6u&_xV!eYVE F{|0hdM!o<5 delta 4466 zcmbuBNv!MUd4`X4?%>ON&((Eu(=1%KCrB0!1V@|)18K}rq&UwM(=?I9L83?vlqgDS zQ{5~Iw?P6p2?_=FCYz#*E>a+$)nuHL=4Xt5VRJHGJ>arfPe&)j9c68%op`je>rozCTThd$qb z(thOzdD3oozIFZjlk@w%%R6WOSKEJn=epxwoS%U0mpYryqBHA6oiBF6j@#LP>sQ|Y zr5okC{a=s2VLU#MmaR{o_r3Pl8n-W;?}P2v?*CEe?auFazWTivU;kL=cK!PN< zXbkm>^JC-j%U3_{6rHp)>U_Sj({J$C+gCpy-f4a6TSnKgNM&6ZcT1({^GaVzjkZ#Z|D2Z+T9!cS^KwdO@w>bcUp9-eQxFL zmu^1(we~;W7QJ)6|K@Gco9)+biw5V_+wH4cpQmrU`*yq0WurTd?l!vD=zgOY8a-$P z+`Rkt!&lJ~W5d7@jMNJRjmA<1m(FhQ34h zZG{!lHVi_XK{EPnRBiA?gt}p+i^Ttn4j~ZPydfe#t_mf61Hu}iTPfs7c_w32N z)(01l{^}9;0Ds{R?|!oNK{KnrZryy)diwbBqpK#ReecOd=j->LG}rIkd-Ba=`|84N zwH{o&_35vExpi^=%huCRo!aeJZx4p=OWL(%)7OS2d9=e{%lz>93ZP07zoI3Ere&5k zd3rxb=RN8;8ch;b3`C`p{iBMPtUboPQiBI*6mTxMhO&)r%`IO6^qr9k>pfL32}222 zcv!E;1hkf=%wL{1L)V)c)kGT43vS8zX_uQiD3Z*}z}83{2CMxZ5WL>d7%8EXr*K}GC+AphX%rk9&b266xpPJ@Ki{TeX|P2 z)ZP~~%VX@m(3OVE;=s?VasZn#Gj)iZbBiU|-7f^hM3ZzUF=N0wV#tIopDisNJ=s~bfsj|sd&%sTbinTeV#YT7+uI~b_UHb-4T}%!%)dlY)94fIua*tsL6oB4-s%FW?FvQmM)7^-fArG^rO#b zkXAgiT0O;+3hlON8xKI-~02|OAc=<~UgZ5Y=CHmkZibq(*R>X}7xF|<)gt`3Shll0`R%@1JQ z=;QMNmbo5yG+Egs$R*V86N|}StK?RFJ#yrL3+w-BwvQs?swGogfCKx1v* zhsU$Mk5%}dRPDe7>1w;u_IAk6_1&}r($f$bZ?wKPJQhQHWqBegrF{Wx2F>UuzQsW* z9^ec|c-^ue_{wpasRN>UHkM{MGv#9n5F^tYm4)D^KB*=iSbIkemxYQw$Y|(r9BLJZ znhqFya>VldVB`~FxiB@Xu46{g#FGw84){=CRfo-SJWrxEz7}n@cPx&JeaUgy#Leea zbgc3f#gvF_9JHaH5z}HI61;|PhDQ$otr|8>nMqAfD&)wRkHWA!g;KVwiB8ntucN$J zM3aH*tkK%CyI3Cz`pI#irwjHJ>-q$kjeBb>^ST*jiR*I3MS&h3=q~D1OIuj=cIjw; zupz1Iu)a2Nyg*^>#nKRzI!uUwVh+HJ9<${H9xQdu#j)J-?HMvEp#c}VeZk|*Zcl;f z9Z}WFU?D5Pn)eq2CH?GYftQ~%hS#`X$ex>jy8HA`+YPM0-g~qGZjsiy_uY$o-)(^Q z2My4)%LZrRM-Nn+QUQ|52nYB?}EL)5zHXhj#2@ep}d)z6B-n=tQJ7EDDm!gN(!XeT>-td z2u&>3tgwBKj|*5Fj*Nkj<_jwaRqCK)RgChQA~DpOP%*{h&Ev_=hAgr4F^wdMDG+Jm z)IfF)vNpz?Y|PUBa89Fxc|4tMRo_Aw+h>Or3L#rZ)3=~A)XBA~mRk;m1B}RUphx!S z6J}oQXRuHOQ_48dlf!`DZG%W=5-3)8+cdyrQS$V|&KwO+<}Q|ky%CoR*9>_L0{gMI z#s{Ll42${N?RkMcZkmXMZrQ`R6_gu-d!)>t;~vc?>eA9;CX(GPOaM$jAJ6)AY8TsB z$fc@J<})=VsdX_VCi!$Y8p%pD&u8JBUZp1njc8X#RE}NG?9#I(90! zm#Mmkh3mTg;Z9W!TTjkl2vNH58sDi0NY}lAxC$ktFBHcljw+B3so#L-eb3$0+;&dElM|l>Ev}CRNdVC;R2(im!P&HE^7xDhX4S8#Y z7ET*Rx86;>?V+nEXjq~czrtLoS~Qz0x@S$ghzi<(H^QdbV;LDBAZsxN!)nfVp{OU$ zla)u+-XxETWV!1_LvuDHPdk_lSgIV-24-2K!be7;!&ki;lXDwrsJ)WdY!+@0+}6;k zRVG3Du)pYqAZ~~C)SHc=tV(Q7N9UXEOl1%0j-Wnlxu-w@(69SorS<)?FyljBGMP^m{Q=)LPAQigJ7Y*Kq} zH}Zmkm>?O|TP_%>M&L5&=H}{%)`Bb!y{JsH#USSy!XE<5NQ=suFC5aYf%9N#v8b3f zz^6l1N}UtUdXU08mD@0c=E)$H@VvlzJTauBhaDWb1{~X2E;H9R_j{k3(_rOd7_F@j zJW)BGU@yF=V3u3o0Cf=76LNR(%OZ5*ajwnG(olQ=gt1J3Q*sqnd-O<+wqjI)u&b`- z%Tp>*YUXVj2l5x`ZZ{AjH%K@Mu|gBx;-kPXS3&`Vqhu10@Iolvq*n9+RUqm7Lxu$P zYVX(eW(Hy+BgRk~BLzQOVWjU>xG?u5K2y5DsH-79+K&gD=5BBLUcL)iSl-Adxp&CM z4!n&CEU|-#+lB($9dFeF?hj-mM|QHjv*axYOhPjW>+)cPRMv$uf*31XX@ks}xsIi_ z*nDTWD44`ki|NUsF)HfF(ZKm~>&2G2cOo1ZtBkt2?U@dH8K?ycSWKuu&^R*XGorKkYj-MeVUv9`Q|NGlz!hp(s5pf{8J3bJM) nThAx6jbdUty_(6yCORA>(E=2.1.4, typescript@^2.0.0, typescript@^2.0.3: +typescript@>=2.1.4, typescript@^2.0.0, typescript@^2.0.3, typescript@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.2.1.tgz#4862b662b988a4c8ff691cc7969622d24db76ae9" @@ -4538,44 +4356,6 @@ yargs@~3.10.0: decamelize "^1.0.0" window-size "0.1.0" -yarn@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/yarn/-/yarn-0.21.3.tgz#8dda3a63c798b383cfa577452c2b3cb3e4aa87e0" - dependencies: - babel-runtime "^6.0.0" - bytes "^2.4.0" - camelcase "^3.0.0" - chalk "^1.1.1" - cmd-shim "^2.0.1" - commander "^2.9.0" - death "^1.0.0" - debug "^2.2.0" - defaults "^1.0.3" - detect-indent "^5.0.0" - ini "^1.3.4" - inquirer "^3.0.1" - invariant "^2.2.0" - is-builtin-module "^1.0.0" - is-ci "^1.0.10" - leven "^2.0.0" - loud-rejection "^1.2.0" - minimatch "^3.0.3" - mkdirp "^0.5.1" - node-emoji "^1.0.4" - node-gyp "^3.2.1" - object-path "^0.11.2" - proper-lockfile "^2.0.0" - read "^1.0.7" - request "^2.75.0" - request-capture-har "^1.1.4" - rimraf "^2.5.0" - roadrunner "^1.1.0" - semver "^5.1.0" - strip-bom "^3.0.0" - tar "^2.2.1" - tar-stream "^1.5.2" - validate-npm-package-license "^3.0.1" - yauzl@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" From 32827d7ba4ddcac16296275ff4770cb083277fbc Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Tue, 7 Mar 2017 17:34:43 -0800 Subject: [PATCH 04/14] more tests for DockerfileService --- static/js/quay.module.ts | 1 + .../dockerfile.service.impl.spec.ts | 200 +++++++++++++++--- .../dockerfile/dockerfile.service.impl.ts | 55 ++--- 3 files changed, 202 insertions(+), 54 deletions(-) diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index a8b946fd1..c375d625f 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -48,6 +48,7 @@ export class quay { // TODO: Make injected values into services and move to NgModule.providers, as constants are not supported in Angular 2 angular .module(quay.name) + .factory("FileReaderFactory", () => () => new FileReader()) .constant('NAME_PATTERNS', NAME_PATTERNS) .constant('INJECTED_CONFIG', INJECTED_CONFIG) .constant('INJECTED_FEATURES', INJECTED_FEATURES) diff --git a/static/js/services/dockerfile/dockerfile.service.impl.spec.ts b/static/js/services/dockerfile/dockerfile.service.impl.spec.ts index 041f31e64..0848a85d4 100644 --- a/static/js/services/dockerfile/dockerfile.service.impl.spec.ts +++ b/static/js/services/dockerfile/dockerfile.service.impl.spec.ts @@ -2,6 +2,116 @@ import { DockerfileServiceImpl, DockerfileInfoImpl } from './dockerfile.service. import Spy = jasmine.Spy; +describe("DockerfileServiceImpl", () => { + var dockerfileServiceImpl: DockerfileServiceImpl; + var dataFileServiceMock: any; + var configMock: any; + var fileReaderMock: FileReader; + + beforeEach(() => { + dataFileServiceMock = jasmine.createSpyObj('dataFileServiceMock', [ + 'readDataArrayAsPossibleArchive', + 'arrayToString', + 'blobToString', + ]); + configMock = jasmine.createSpyObj('configMock', ['getDomain']); + fileReaderMock = new FileReader(); + dockerfileServiceImpl = new DockerfileServiceImpl(dataFileServiceMock, configMock, () => fileReaderMock); + }); + + describe("getDockerfile", () => { + var file: any; + var readAsFileBufferSpy: Spy; + + beforeEach(() => { + dataFileServiceMock.readDataArrayAsPossibleArchive.and.callFake((buf, success, failure) => { + var files: any[] = []; + failure(files); + }); + + dataFileServiceMock.arrayToString.and.callFake((buf, callback) => { + var contents: string = ""; + callback(contents); + }); + + dataFileServiceMock.blobToString.and.callFake((blob, callback) => { + var contents: string = ""; + callback(contents); + }); + + spyOn(DockerfileInfoImpl, "forData").and.returnValue(null); + readAsFileBufferSpy = spyOn(fileReaderMock, "readAsArrayBuffer").and.callFake(() => { + var event: any = {target: {result: file}}; + fileReaderMock.onload(event); + }); + + file = "FROM quay.io/coreos/nginx:latest"; + }); + + it("calls datafile service to read given file as possible archive file", (done) => { + dockerfileServiceImpl.getDockerfile(file, + (dockerfile: DockerfileInfoImpl) => { + fail("Should not invoke success callback"); + done(); + }, + (error: Event | string) => { + expect(readAsFileBufferSpy.calls.argsFor(0)[0]).toEqual(file); + expect(dataFileServiceMock.readDataArrayAsPossibleArchive).toHaveBeenCalled(); + done(); + }); + }); + + it("calls datafile service to convert file to string if given file is not an archive", (done) => { + dockerfileServiceImpl.getDockerfile(file, + (dockerfile: DockerfileInfoImpl) => { + fail("Should not invoke success callback"); + done(); + }, + (error: Event | string) => { + expect(dataFileServiceMock.arrayToString.calls.argsFor(0)[0]).toEqual(file); + done(); + }); + }); + + it("calls failure callback if given non-archive file that is not a valid Dockerfile", (done) => { + dockerfileServiceImpl.getDockerfile(file, + (dockerfile: DockerfileInfoImpl) => { + fail("Should not invoke success callback"); + done(); + }, + (error: Event | string) => { + expect(error).toEqual('File chosen is not a valid Dockerfile'); + done(); + }); + }); + + it("calls success callback with new DockerfileInfoImpl instance if given valid Dockerfile", (done) => { + done(); + }); + + it("calls failure callback if given archive file with no Dockerfile present in root directory", (done) => { + done(); + }); + + it("calls datafile service to convert blob to string if given file is an archive", (done) => { + done(); + }); + + it("calls failure callback if given archive file with invalid Dockerfile", (done) => { + done(); + }); + + it("calls success callback with new DockerfileInfoImpl instance if given archive with valid Dockerfile", (done) => { + done(); + }); + }); + + describe("extractDockerfile", () => { + // TODO + }); +}); + + describe("DockerfileInfoImpl", () => { var dockerfileInfoImpl: DockerfileInfoImpl; var contents: string; @@ -27,18 +137,75 @@ describe("DockerfileInfoImpl", () => { }); describe("getRegistryBaseImage", () => { + var domain: string; + beforeEach(() => { + domain = "quay.io"; + }); + it("returns null if instance's contents do not contain a 'FROM' command", () => { + var getBaseImageSpy: Spy = spyOn(dockerfileInfoImpl, "getBaseImage").and.returnValue(null); + + expect(dockerfileInfoImpl.getRegistryBaseImage()).toBe(null); + expect(getBaseImageSpy).toHaveBeenCalled(); + }); + + it("returns null if the domain of the instance's config does not match that of the base image", () => { + configMock.getDomain.and.returnValue(domain); + spyOn(dockerfileInfoImpl, "getBaseImage").and.returnValue('host.com'); + + expect(dockerfileInfoImpl.getRegistryBaseImage()).toBe(null); + expect(configMock.getDomain).toHaveBeenCalled(); + }); + + it("returns the registry base image", () => { + spyOn(dockerfileInfoImpl, "getBaseImage").and.returnValue(null); + }); }); describe("getBaseImage", () => { - var baseImageAndTag + var host: string; + var port: number; + var tag: string; + var image: string; - it("returns null if instance's contents do not contain a 'FROM' command", () => { - var getBaseImageAndTagSpy: Spy = spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue() + beforeEach(() => { + host = 'quay.io'; + port = 80; + tag = 'latest'; + image = 'coreos/nginx'; }); - it("returns") + it("returns null if instance's contents do not contain a 'FROM' command", () => { + var getBaseImageAndTagSpy: Spy = spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(null); + + expect(dockerfileInfoImpl.getBaseImage()).toBe(null); + expect(getBaseImageAndTagSpy).toHaveBeenCalled(); + }); + + it("returns the image name if in the format 'someimage'", () => { + spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(image); + + expect(dockerfileInfoImpl.getBaseImage()).toEqual(image); + }); + + it("returns the image name if in the format 'someimage:tag'", () => { + spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(`${image}:${tag}`); + + expect(dockerfileInfoImpl.getBaseImage()).toEqual(image); + }); + + it("returns the host, port, and image name if in the format 'host:port/someimage'", () => { + spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(`${host}:${port}/${image}`); + + expect(dockerfileInfoImpl.getBaseImage()).toEqual(`${host}:${port}/${image}`); + }); + + it("returns the host, port, and image name if in the format 'host:port/someimage:tag'", () => { + spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(`${host}:${port}/${image}:${tag}`); + + expect(dockerfileInfoImpl.getBaseImage()).toEqual(`${host}:${port}/${image}`); + }); }); describe("getBaseImageAndTag", () => { @@ -64,28 +231,3 @@ describe("DockerfileInfoImpl", () => { }); }); }); - - -describe("DockerfileServiceImpl", () => { - var dockerfileServiceImpl: DockerfileServiceImpl; - var dataFileServiceMock: any; - var configMock: any; - - beforeEach(() => { - dataFileServiceMock = jasmine.createSpyObj('dataFileServiceMock', [ - 'readDataArrayAsPossibleArchive', - 'arrayToString', - 'blobToString', - ]); - configMock = jasmine.createSpyObj('configMock', ['getDomain']); - dockerfileServiceImpl = new DockerfileServiceImpl(dataFileServiceMock, configMock); - }); - - describe("getDockerfile", () => { - - }); - - describe("extractDockerfile", () => { - - }); -}); \ No newline at end of file diff --git a/static/js/services/dockerfile/dockerfile.service.impl.ts b/static/js/services/dockerfile/dockerfile.service.impl.ts index fbeb02c71..6783e8786 100644 --- a/static/js/services/dockerfile/dockerfile.service.impl.ts +++ b/static/js/services/dockerfile/dockerfile.service.impl.ts @@ -5,21 +5,32 @@ import { Injectable } from 'angular-ts-decorators'; @Injectable(DockerfileService.name) export class DockerfileServiceImpl implements DockerfileService { - constructor(private DataFileService: any, private Config: any) { - console.log(`=================== DockerfileServiceImpl ==========================`); + constructor(private DataFileService: any, private Config: any, private FileReaderFactory: () => FileReader) { + + } + + public extractDockerfile(file: any): Promise { + return new Promise((resolve, reject) => { + // TODO: Replace callbacks with promises + }); } public getDockerfile(file: any, success: (dockerfile: DockerfileInfoImpl) => void, failure: (error: Event | string) => void): void { - var reader: FileReader = new FileReader(); - reader.onload = (event: Event) => { - var dataArray: any = reader.result; - this.DataFileService.readDataArrayAsPossibleArchive(dataArray, (files) => { - this.processFiles(files, dataArray, success, failure); - }, () => { + var reader: FileReader = this.FileReaderFactory(); + reader.onload = (event: any) => { + + // FIXME: Debugging + console.log(event.target.result); + + this.DataFileService.readDataArrayAsPossibleArchive(event.target.result, + (files: any[]) => { + this.processFiles(files, event.target.result, success, failure); + }, + () => { // Not an archive. Read directly as a single file. - this.processFiles([], dataArray, success, failure); + this.processFiles([], event.target.result, success, failure); }); }; @@ -27,13 +38,7 @@ export class DockerfileServiceImpl implements DockerfileService { reader.readAsArrayBuffer(file); } - public extractDockerfile(file: any): Promise { - return new Promise((resolve, reject) => { - // TODO: Replace callbacks with promise - }); - } - - private processFiles(files: any, + private processFiles(files: any[], dataArray: any[], success: (dockerfile: DockerfileInfoImpl) => void, failure: (error: ErrorEvent | string) => void): void { @@ -41,8 +46,8 @@ export class DockerfileServiceImpl implements DockerfileService { // treat it as a single Dockerfile. if (files.length == 0) { this.DataFileService.arrayToString(dataArray, (contents: string) => { - var result = DockerfileInfoImpl.forData(contents, Object.assign({}, this.Config)); - if (!result) { + var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, this.Config); + if (result == null) { failure('File chosen is not a valid Dockerfile'); return; } @@ -56,8 +61,8 @@ export class DockerfileServiceImpl implements DockerfileService { files.forEach((file) => { if (file['name'] == 'Dockerfile') { this.DataFileService.blobToString(file.toBlob(), (contents: string) => { - var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, Object.assign({}, this.Config)); - if (result != null) { + var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, this.Config); + if (result == null) { failure('Dockerfile inside archive is not a valid Dockerfile'); return; } @@ -104,7 +109,7 @@ export class DockerfileInfoImpl implements DockerfileInfo { } public getBaseImage(): string | null { - var imageAndTag = this.getBaseImageAndTag(); + const imageAndTag = this.getBaseImageAndTag(); if (!imageAndTag) { return null; } @@ -114,15 +119,15 @@ export class DockerfileInfoImpl implements DockerfileInfo { // 2) someimage:tag // 3) host:port/someimage // 4) host:port/someimage:tag - var lastIndex = imageAndTag.lastIndexOf(':'); - if (lastIndex < 0) { + const lastIndex: number = imageAndTag.lastIndexOf(':'); + if (lastIndex == -1) { return imageAndTag; } // Otherwise, check if there is a / in the portion after the split point. If so, // then the latter is part of the path (and not a tag). - var afterColon = imageAndTag.substring(lastIndex + 1); - if (afterColon.indexOf('/') >= 0) { + const afterColon: string = imageAndTag.substring(lastIndex + 1); + if (afterColon.indexOf('/') != -1) { return imageAndTag; } From 390e3890274eeaea04d82e5e9aa14bb4f49dd22f Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Tue, 7 Mar 2017 21:46:12 -0800 Subject: [PATCH 05/14] all tests for DockerfileServiceImpl --- .../dockerfile.service.impl.spec.ts | 95 +++++++++++++++---- .../dockerfile/dockerfile.service.impl.ts | 46 ++++----- 2 files changed, 99 insertions(+), 42 deletions(-) diff --git a/static/js/services/dockerfile/dockerfile.service.impl.spec.ts b/static/js/services/dockerfile/dockerfile.service.impl.spec.ts index 0848a85d4..9f3ba1747 100644 --- a/static/js/services/dockerfile/dockerfile.service.impl.spec.ts +++ b/static/js/services/dockerfile/dockerfile.service.impl.spec.ts @@ -21,12 +21,14 @@ describe("DockerfileServiceImpl", () => { describe("getDockerfile", () => { var file: any; + var invalidArchiveFile: any[]; + var validArchiveFile: any[]; var readAsFileBufferSpy: Spy; + var forDataSpy: Spy; beforeEach(() => { dataFileServiceMock.readDataArrayAsPossibleArchive.and.callFake((buf, success, failure) => { - var files: any[] = []; - failure(files); + failure([]); }); dataFileServiceMock.arrayToString.and.callFake((buf, callback) => { @@ -35,28 +37,29 @@ describe("DockerfileServiceImpl", () => { }); dataFileServiceMock.blobToString.and.callFake((blob, callback) => { - var contents: string = ""; - callback(contents); + callback(blob.toString()); }); - spyOn(DockerfileInfoImpl, "forData").and.returnValue(null); + forDataSpy = spyOn(DockerfileInfoImpl, "forData").and.returnValue(new DockerfileInfoImpl(file, configMock)); readAsFileBufferSpy = spyOn(fileReaderMock, "readAsArrayBuffer").and.callFake(() => { var event: any = {target: {result: file}}; fileReaderMock.onload(event); }); file = "FROM quay.io/coreos/nginx:latest"; + validArchiveFile = [{name: 'Dockerfile', toBlob: jasmine.createSpy('toBlobSpy').and.returnValue(file)}]; + invalidArchiveFile = [{name: 'main.exe', toBlob: jasmine.createSpy('toBlobSpy').and.returnValue("")}]; }); it("calls datafile service to read given file as possible archive file", (done) => { dockerfileServiceImpl.getDockerfile(file, (dockerfile: DockerfileInfoImpl) => { - fail("Should not invoke success callback"); + expect(readAsFileBufferSpy.calls.argsFor(0)[0]).toEqual(file); + expect(dataFileServiceMock.readDataArrayAsPossibleArchive).toHaveBeenCalled(); done(); }, (error: Event | string) => { - expect(readAsFileBufferSpy.calls.argsFor(0)[0]).toEqual(file); - expect(dataFileServiceMock.readDataArrayAsPossibleArchive).toHaveBeenCalled(); + fail("Should not invoke failure callback"); done(); }); }); @@ -64,16 +67,17 @@ describe("DockerfileServiceImpl", () => { it("calls datafile service to convert file to string if given file is not an archive", (done) => { dockerfileServiceImpl.getDockerfile(file, (dockerfile: DockerfileInfoImpl) => { - fail("Should not invoke success callback"); + expect(dataFileServiceMock.arrayToString.calls.argsFor(0)[0]).toEqual(file); done(); }, (error: Event | string) => { - expect(dataFileServiceMock.arrayToString.calls.argsFor(0)[0]).toEqual(file); + fail("Should not invoke success callback"); done(); }); }); it("calls failure callback if given non-archive file that is not a valid Dockerfile", (done) => { + forDataSpy.and.returnValue(null); dockerfileServiceImpl.getDockerfile(file, (dockerfile: DockerfileInfoImpl) => { fail("Should not invoke success callback"); @@ -86,28 +90,87 @@ describe("DockerfileServiceImpl", () => { }); it("calls success callback with new DockerfileInfoImpl instance if given valid Dockerfile", (done) => { - done(); + dockerfileServiceImpl.getDockerfile(file, + (dockerfile: DockerfileInfoImpl) => { + expect(dockerfile).toBeDefined(); + done(); + }, + (error: Event | string) => { + fail('Should not invoke failure callback'); + done(); + }); }); it("calls failure callback if given archive file with no Dockerfile present in root directory", (done) => { - done(); + dataFileServiceMock.readDataArrayAsPossibleArchive.and.callFake((buf, success, failure) => { + success(invalidArchiveFile); + }); + + dockerfileServiceImpl.getDockerfile(file, + (dockerfile: DockerfileInfoImpl) => { + fail("Should not invoke success callback"); + done(); + }, + (error: Event | string) => { + expect(error).toEqual('No Dockerfile found in root of archive'); + done(); + }); }); it("calls datafile service to convert blob to string if given file is an archive", (done) => { - done(); + dataFileServiceMock.readDataArrayAsPossibleArchive.and.callFake((buf, success, failure) => { + success(validArchiveFile); + }); + + dockerfileServiceImpl.getDockerfile(file, + (dockerfile: DockerfileInfoImpl) => { + expect(validArchiveFile[0].toBlob).toHaveBeenCalled(); + expect(dataFileServiceMock.blobToString.calls.argsFor(0)[0]).toEqual(validArchiveFile[0].toBlob()); + done(); + }, + (error: Event | string) => { + fail("Should not invoke success callback"); + done(); + }); }); it("calls failure callback if given archive file with invalid Dockerfile", (done) => { - done(); + forDataSpy.and.returnValue(null); + invalidArchiveFile[0].name = 'Dockerfile'; + dataFileServiceMock.readDataArrayAsPossibleArchive.and.callFake((buf, success, failure) => { + success(invalidArchiveFile); + }); + + dockerfileServiceImpl.getDockerfile(file, + (dockerfile: DockerfileInfoImpl) => { + fail("Should not invoke success callback"); + done(); + }, + (error: Event | string) => { + expect(error).toEqual('Dockerfile inside archive is not a valid Dockerfile'); + done(); + }); }); it("calls success callback with new DockerfileInfoImpl instance if given archive with valid Dockerfile", (done) => { - done(); + dataFileServiceMock.readDataArrayAsPossibleArchive.and.callFake((buf, success, failure) => { + success(validArchiveFile); + }); + + dockerfileServiceImpl.getDockerfile(file, + (dockerfile: DockerfileInfoImpl) => { + expect(dockerfile).toBeDefined(); + done(); + }, + (error: Event | string) => { + fail('Should not invoke failure callback'); + done(); + }); }); }); describe("extractDockerfile", () => { - // TODO + // TODO: TDD promise-based method with same functionality as getDockerfile }); }); diff --git a/static/js/services/dockerfile/dockerfile.service.impl.ts b/static/js/services/dockerfile/dockerfile.service.impl.ts index 6783e8786..d2060b08d 100644 --- a/static/js/services/dockerfile/dockerfile.service.impl.ts +++ b/static/js/services/dockerfile/dockerfile.service.impl.ts @@ -20,17 +20,13 @@ export class DockerfileServiceImpl implements DockerfileService { failure: (error: Event | string) => void): void { var reader: FileReader = this.FileReaderFactory(); reader.onload = (event: any) => { - - // FIXME: Debugging - console.log(event.target.result); - this.DataFileService.readDataArrayAsPossibleArchive(event.target.result, (files: any[]) => { - this.processFiles(files, event.target.result, success, failure); + this.processFiles(files, success, failure); }, () => { - // Not an archive. Read directly as a single file. - this.processFiles([], event.target.result, success, failure); + // Not an archive. Read directly as a single file. + this.processFile(event.target.result, success, failure); }); }; @@ -38,25 +34,23 @@ export class DockerfileServiceImpl implements DockerfileService { reader.readAsArrayBuffer(file); } + private processFile(dataArray: any, + success: (dockerfile: DockerfileInfoImpl) => void, + failure: (error: ErrorEvent | string) => void): void { + this.DataFileService.arrayToString(dataArray, (contents: string) => { + var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, this.Config); + if (result == null) { + failure('File chosen is not a valid Dockerfile'); + } + else { + success(result); + } + }); + } + private processFiles(files: any[], - dataArray: any[], success: (dockerfile: DockerfileInfoImpl) => void, failure: (error: ErrorEvent | string) => void): void { - // The files array will be empty if the submitted file was not an archive. We therefore - // treat it as a single Dockerfile. - if (files.length == 0) { - this.DataFileService.arrayToString(dataArray, (contents: string) => { - var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, this.Config); - if (result == null) { - failure('File chosen is not a valid Dockerfile'); - return; - } - - success(result); - }); - return; - } - var found: boolean = false; files.forEach((file) => { if (file['name'] == 'Dockerfile') { @@ -64,10 +58,10 @@ export class DockerfileServiceImpl implements DockerfileService { var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, this.Config); if (result == null) { failure('Dockerfile inside archive is not a valid Dockerfile'); - return; } - - success(result); + else { + success(result); + } }); found = true; } From 85441c84595a1fd23494f6dc9881ecc3a8101b6f Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Wed, 8 Mar 2017 11:43:53 -0800 Subject: [PATCH 06/14] refactoring to promises --- karma.conf.js | 4 + package.json | 1 + .../js/directives/ui/dockerfile-build-form.js | 55 +++++++++---- static/js/quay.module.ts | 1 + .../dockerfile.service.impl.spec.ts | 80 ++++++++++++++++++- .../dockerfile/dockerfile.service.impl.ts | 53 +++++++++++- .../services/dockerfile/dockerfile.service.ts | 9 ++- webpack.config.js | 4 +- yarn.lock | 12 +-- 9 files changed, 191 insertions(+), 28 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index 841933cd6..38d9cd46e 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -24,6 +24,9 @@ module.exports = function(config) { 'node_modules/raven-js/dist/raven.js', 'node_modules/cal-heatmap/cal-heatmap.js', + // Polyfills + 'node_modules/core-js/index.js', + // static/lib resources 'static/lib/**/*.js', @@ -37,6 +40,7 @@ module.exports = function(config) { preprocessors: { 'static/lib/ngReact/react.ngReact.min.js': ['webpack'], 'static/lib/angular-moment.min.js': ['webpack'], + 'node_modules/core-js/index.js': ['webpack'], 'static/js/**/*.spec.ts*': ['webpack'], }, webpack: webpackConfig, diff --git a/package.json b/package.json index f509e7e51..a18699fd4 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "bootstrap": "^3.3.2", "bootstrap-datepicker": "^1.6.4", "cal-heatmap": "^3.3.10", + "core-js": "^2.4.1", "d3": "^3.3.3", "eonasdan-bootstrap-datetimepicker": "^4.17.43", "jquery": "1.12.4", diff --git a/static/js/directives/ui/dockerfile-build-form.js b/static/js/directives/ui/dockerfile-build-form.js index fdd705f21..8fae11bb7 100644 --- a/static/js/directives/ui/dockerfile-build-form.js +++ b/static/js/directives/ui/dockerfile-build-form.js @@ -39,25 +39,48 @@ angular.module('quay').directive('dockerfileBuildForm', function () { $scope.state = 'checking'; $scope.selectedFiles = files; - DockerfileService.getDockerfile(files[0], function(df) { - var baseImage = df.getRegistryBaseImage(); - if (baseImage) { - checkPrivateImage(baseImage); - } else { - $scope.state = 'ready'; - } + // FIXME: Remove this + // DockerfileService.getDockerfile(files[0], function(df) { + // var baseImage = df.getRegistryBaseImage(); + // if (baseImage) { + // checkPrivateImage(baseImage); + // } else { + // $scope.state = 'ready'; + // } + // + // $scope.$apply(function() { + // opt_callback && opt_callback(true, 'Dockerfile found and valid') + // }); + // }, function(msg) { + // $scope.state = 'empty'; + // $scope.privateBaseRepository = null; + // + // $scope.$apply(function() { + // opt_callback && opt_callback(false, msg || 'Could not find valid Dockerfile'); + // }); + // }); - $scope.$apply(function() { - opt_callback && opt_callback(true, 'Dockerfile found and valid') - }); - }, function(msg) { - $scope.state = 'empty'; - $scope.privateBaseRepository = null; + DockerfileService.extractDockerfile(files[0]) + .then(function(dockerfileInfo) { + var baseImage = dockerfileInfo.getRegistryBaseImage(); + if (baseImage) { + checkPrivateImage(baseImage); + } else { + $scope.state = 'ready'; + } - $scope.$apply(function() { - opt_callback && opt_callback(false, msg || 'Could not find valid Dockerfile'); + $scope.$apply(function() { + opt_callback && opt_callback(true, 'Dockerfile found and valid') + }); + }) + .catch(function(error) { + $scope.state = 'empty'; + $scope.privateBaseRepository = null; + + $scope.$apply(function() { + opt_callback && opt_callback(false, error || 'Could not find valid Dockerfile'); + }); }); - }); }; $scope.handleFilesCleared = function() { diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index c375d625f..3b558e5d9 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -1,4 +1,5 @@ import * as angular from "angular"; +import 'core-js'; import { ViewArrayImpl } from "./services/view-array/view-array.impl"; import { NAME_PATTERNS } from "./constants/name-patterns.constant"; import { INJECTED_CONFIG, INJECTED_FEATURES, INJECTED_ENDPOINTS } from "./constants/injected-values.constant"; diff --git a/static/js/services/dockerfile/dockerfile.service.impl.spec.ts b/static/js/services/dockerfile/dockerfile.service.impl.spec.ts index 9f3ba1747..20ad4f6f1 100644 --- a/static/js/services/dockerfile/dockerfile.service.impl.spec.ts +++ b/static/js/services/dockerfile/dockerfile.service.impl.spec.ts @@ -170,7 +170,77 @@ describe("DockerfileServiceImpl", () => { }); describe("extractDockerfile", () => { - // TODO: TDD promise-based method with same functionality as getDockerfile + var file: any; + var invalidArchiveFile: any[]; + var validArchiveFile: any[]; + var readAsFileBufferSpy: Spy; + var forDataSpy: Spy; + + beforeEach(() => { + dataFileServiceMock.readDataArrayAsPossibleArchive.and.callFake((buf, success, failure) => { + failure([]); + }); + + dataFileServiceMock.arrayToString.and.callFake((buf, callback) => { + var contents: string = ""; + callback(contents); + }); + + dataFileServiceMock.blobToString.and.callFake((blob, callback) => { + callback(blob.toString()); + }); + + forDataSpy = spyOn(DockerfileInfoImpl, "forData").and.returnValue(new DockerfileInfoImpl(file, configMock)); + readAsFileBufferSpy = spyOn(fileReaderMock, "readAsArrayBuffer").and.callFake(() => { + var event: any = {target: {result: file}}; + fileReaderMock.onload(event); + }); + + file = "FROM quay.io/coreos/nginx:latest"; + validArchiveFile = [{name: 'Dockerfile', toBlob: jasmine.createSpy('toBlobSpy').and.returnValue(file)}]; + invalidArchiveFile = [{name: 'main.exe', toBlob: jasmine.createSpy('toBlobSpy').and.returnValue("")}]; + }); + + it("calls datafile service to read given file as possible archive file", (done) => { + dockerfileServiceImpl.extractDockerfile(file) + .then((dockerfile: DockerfileInfoImpl) => { + expect(readAsFileBufferSpy.calls.argsFor(0)[0]).toEqual(file); + expect(dataFileServiceMock.readDataArrayAsPossibleArchive).toHaveBeenCalled(); + done(); + }) + .catch((error: string) => { + fail('Promise should be resolved'); + done(); + }); + }); + + it("calls datafile service to convert file to string if given file is not an archive", (done) => { + done(); + }); + + it("returns rejected promise if given non-archive file that is not a valid Dockerfile", (done) => { + done(); + }); + + it("returns resolved promise with new DockerfileInfoImpl instance if given valid Dockerfile", (done) => { + done(); + }); + + it("returns rejected promise if given archive file with no Dockerfile present in root directory", (done) => { + done(); + }); + + it("calls datafile service to convert blob to string if given file is an archive", (done) => { + done(); + }); + + it("returns rejected promise if given archive file with invalid Dockerfile", (done) => { + done(); + }); + + it("returns resolved promise of new DockerfileInfoImpl instance if given archive with valid Dockerfile", (done) => { + done(); + }); }); }); @@ -201,9 +271,13 @@ describe("DockerfileInfoImpl", () => { describe("getRegistryBaseImage", () => { var domain: string; + var baseImage: string; beforeEach(() => { domain = "quay.io"; + baseImage = "coreos/nginx"; + + configMock.getDomain.and.returnValue(domain); }); it("returns null if instance's contents do not contain a 'FROM' command", () => { @@ -222,7 +296,9 @@ describe("DockerfileInfoImpl", () => { }); it("returns the registry base image", () => { - spyOn(dockerfileInfoImpl, "getBaseImage").and.returnValue(null); + spyOn(dockerfileInfoImpl, "getBaseImage").and.returnValue(`${domain}/${baseImage}`); + + expect(dockerfileInfoImpl.getRegistryBaseImage()).toEqual(baseImage); }); }); diff --git a/static/js/services/dockerfile/dockerfile.service.impl.ts b/static/js/services/dockerfile/dockerfile.service.impl.ts index d2060b08d..f865ae70f 100644 --- a/static/js/services/dockerfile/dockerfile.service.impl.ts +++ b/static/js/services/dockerfile/dockerfile.service.impl.ts @@ -11,7 +11,20 @@ export class DockerfileServiceImpl implements DockerfileService { public extractDockerfile(file: any): Promise { return new Promise((resolve, reject) => { - // TODO: Replace callbacks with promises + var reader: FileReader = this.FileReaderFactory(); + reader.onload = (event: any) => { + this.DataFileService.readDataArrayAsPossibleArchive(event.target.result, + (files: any[]) => { + this.processFiles1(files); + }, + () => { + // Not an archive. Read directly as a single file. + this.processFile1(event.target.result); + }); + }; + + reader.onerror = (event: any) => reject(event); + reader.readAsArrayBuffer(file); }); } @@ -48,6 +61,20 @@ export class DockerfileServiceImpl implements DockerfileService { }); } + private processFile1(dataArray: any): Promise { + return new Promise((resolve, reject) => { + this.DataFileService.arrayToString(dataArray, (contents: string) => { + var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, this.Config); + if (result == null) { + reject('File chosen is not a valid Dockerfile'); + } + else { + resolve(result); + } + }); + }); + } + private processFiles(files: any[], success: (dockerfile: DockerfileInfoImpl) => void, failure: (error: ErrorEvent | string) => void): void { @@ -71,6 +98,30 @@ export class DockerfileServiceImpl implements DockerfileService { failure('No Dockerfile found in root of archive'); } } + + private processFiles1(files: any[]): Promise { + return new Promise((resolve, reject) => { + var found: boolean = false; + files.forEach((file) => { + if (file['name'] == 'Dockerfile') { + this.DataFileService.blobToString(file.toBlob(), (contents: string) => { + var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, this.Config); + if (result == null) { + reject('Dockerfile inside archive is not a valid Dockerfile'); + } + else { + resolve(result); + } + }); + found = true; + } + }); + + if (!found) { + reject('No Dockerfile found in root of archive'); + } + }); + } } diff --git a/static/js/services/dockerfile/dockerfile.service.ts b/static/js/services/dockerfile/dockerfile.service.ts index db62913c2..3af34b0ee 100644 --- a/static/js/services/dockerfile/dockerfile.service.ts +++ b/static/js/services/dockerfile/dockerfile.service.ts @@ -5,8 +5,8 @@ export abstract class DockerfileService { /** - * Retrieve Dockerfile from given archive file. - * @param file File containing Dockerfile. + * Retrieve Dockerfile from given file. + * @param file Dockerfile or archive file containing Dockerfile. * @param success Success callback with retrieved Dockerfile as parameter. * @param failure Failure callback with failure message as parameter. */ @@ -14,6 +14,11 @@ export abstract class DockerfileService { success: (dockerfile: DockerfileInfo) => void, failure: (error: ErrorEvent | string) => void): void; + /** + * Retrieve Dockerfile from given file. + * @param file Dockerfile or archive file containing Dockerfile. + * @return promise Promise resolving to new DockerfileInfo instance or rejecting to error message. + */ public abstract extractDockerfile(file: any): Promise; } diff --git a/webpack.config.js b/webpack.config.js index 788f51445..68f51889c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -21,7 +21,9 @@ var config = { rules: [ { test: /\.tsx?$/, - loader: "ts-loader", + use: [ + "ts-loader", + ], exclude: /node_modules/ }, { diff --git a/yarn.lock b/yarn.lock index 6a2b4aa68..08b9adf09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -796,7 +796,7 @@ core-js@^1.0.0: version "1.2.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" -core-js@^2.1.0: +core-js@^2.1.0, core-js@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" @@ -3514,14 +3514,14 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" -"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@~4.3.3: - version "4.3.6" - resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" - -semver@^5.0.1, semver@^5.0.3, semver@^5.1.0, semver@~5.3.0: +"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.1.0, semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" +semver@~4.3.3: + version "4.3.6" + resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" + semver@~5.0.1: version "5.0.3" resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a" From 4303f658e83a4c15f3954c710949c8383ac452ef Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Fri, 10 Mar 2017 16:01:41 -0800 Subject: [PATCH 07/14] dockerfile-build-form component working with promise-based method --- .../dockerfile/dockerfile.service.impl.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/static/js/services/dockerfile/dockerfile.service.impl.ts b/static/js/services/dockerfile/dockerfile.service.impl.ts index 5cb861749..e2826b54c 100644 --- a/static/js/services/dockerfile/dockerfile.service.impl.ts +++ b/static/js/services/dockerfile/dockerfile.service.impl.ts @@ -15,14 +15,19 @@ export class DockerfileServiceImpl implements DockerfileService { return new Promise((resolve, reject) => { var reader: FileReader = this.fileReaderFactory(); reader.onload = (event: any) => { - // FIXME: Debugging - console.log(event.target.result); - this.DataFileService.readDataArrayAsPossibleArchive(event.target.result, (files: any[]) => { - this.processFiles(files) - .then((dockerfileInfo: DockerfileInfoImpl) => resolve(dockerfileInfo)) - .catch((error: string) => reject(error)); + if (files.length > 0) { + this.processFiles(files) + .then((dockerfileInfo: DockerfileInfoImpl) => resolve(dockerfileInfo)) + .catch((error: string) => reject(error)); + } + // Not an archive. Read directly as a single file. + else { + this.processFile(event.target.result) + .then((dockerfileInfo: DockerfileInfoImpl) => resolve(dockerfileInfo)) + .catch((error: string) => reject(error)); + } }, () => { // Not an archive. Read directly as a single file. From 40e6540074dffee9200c7c683ad6ca0443061b9e Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Sat, 11 Mar 2017 15:27:23 -0800 Subject: [PATCH 08/14] easier mocking/spying using ts-mocks --- package.json | 1 + .../js/directives/ui/dockerfile-build-form.js | 2 - .../datafile/datafile.service.impl.spec.ts | 0 .../datafile/datafile.service.impl.ts | 0 .../js/services/datafile/datafile.service.ts | 48 +++++++++++++++++++ .../dockerfile.service.impl.spec.ts | 37 +++++++------- .../dockerfile/dockerfile.service.impl.ts | 3 +- yarn.lock | 4 ++ 8 files changed, 73 insertions(+), 22 deletions(-) create mode 100644 static/js/services/datafile/datafile.service.impl.spec.ts create mode 100644 static/js/services/datafile/datafile.service.impl.ts create mode 100644 static/js/services/datafile/datafile.service.ts diff --git a/package.json b/package.json index 4fb9f445c..8d21d6401 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "source-map-loader": "0.1.5", "style-loader": "0.13.1", "ts-loader": "^0.9.5", + "ts-mocks": "^0.2.2", "typescript": "^2.2.1", "typings": "1.4.0", "webpack": "^2.2" diff --git a/static/js/directives/ui/dockerfile-build-form.js b/static/js/directives/ui/dockerfile-build-form.js index 136f29118..2a6ef94de 100644 --- a/static/js/directives/ui/dockerfile-build-form.js +++ b/static/js/directives/ui/dockerfile-build-form.js @@ -10,10 +10,8 @@ angular.module('quay').directive('dockerfileBuildForm', function () { restrict: 'C', scope: { 'repository': '=repository', - 'isReady': '=?isReady', 'reset': '=?reset', - 'readyForBuild': '&readyForBuild' }, controller: function($scope, $element, ApiService, DockerfileService, Config) { diff --git a/static/js/services/datafile/datafile.service.impl.spec.ts b/static/js/services/datafile/datafile.service.impl.spec.ts new file mode 100644 index 000000000..e69de29bb diff --git a/static/js/services/datafile/datafile.service.impl.ts b/static/js/services/datafile/datafile.service.impl.ts new file mode 100644 index 000000000..e69de29bb diff --git a/static/js/services/datafile/datafile.service.ts b/static/js/services/datafile/datafile.service.ts new file mode 100644 index 000000000..43bae9d11 --- /dev/null +++ b/static/js/services/datafile/datafile.service.ts @@ -0,0 +1,48 @@ +/** + * Service which provides helper methods for downloading a data file from a URL, and extracting + * its contents as .tar, .tar.gz, or .zip file. Note that this service depends on external + * library code in the lib/ directory: + * - jszip.min.js + * - Blob.js + * - zlib.js + */ +export abstract class DatafileService { + + /** + * Convert a blob to a string. + * @param blob The blob to convert. + * @param callback The success callback given converted blob. + */ + public abstract blobToString(blob: any, callback: (result: string) => void): void; + + /** + * Convert array to string. + * @param buf The array buffer to convert. + * @param callback The success callback given converted array buffer. + */ + public abstract arrayToString(buf: any, callback: (result: string) => void): void; + + /** + * Determine if a given data array is an archive file. + * @param buf The data array to check. + * @param success The success callback if the given array is an archive file, given the file contents. + * @param failure The failure callback if the given array is not an archive file, given the error message. + */ + public abstract readDataArrayAsPossibleArchive(buf: any, + success: (result: any) => void, + failure: (error: any) => void): void; + + /** + * Download a file into an array buffer while tracking progress. + * @param $scope An AngularJS $scope instance. + * @param url The URL of the file to be downloaded. + * @param progress The callback for download progress. + * @param error The error callback. + * @param loaded The success callback given the downloaded array buffer. + */ + public abstract downloadDataFileAsArrayBuffer($scope: ng.IScope, + url: string, + progress: (percent: number) => void, + error: () => void, + loaded: (uint8array: Uint8Array) => void): void; +} \ No newline at end of file diff --git a/static/js/services/dockerfile/dockerfile.service.impl.spec.ts b/static/js/services/dockerfile/dockerfile.service.impl.spec.ts index edbb8175a..ebccbc2ea 100644 --- a/static/js/services/dockerfile/dockerfile.service.impl.spec.ts +++ b/static/js/services/dockerfile/dockerfile.service.impl.spec.ts @@ -1,22 +1,22 @@ import { DockerfileServiceImpl, DockerfileInfoImpl } from './dockerfile.service.impl'; +import { DatafileService } from '../datafile/datafile.service'; import Spy = jasmine.Spy; +import { Mock } from 'ts-mocks'; describe("DockerfileServiceImpl", () => { var dockerfileServiceImpl: DockerfileServiceImpl; - var dataFileServiceMock: any; + var datafileServiceMock: Mock; + var datafileService: DatafileService; var configMock: any; var fileReaderMock: FileReader; beforeEach(() => { - dataFileServiceMock = jasmine.createSpyObj('dataFileServiceMock', [ - 'readDataArrayAsPossibleArchive', - 'arrayToString', - 'blobToString', - ]); + datafileServiceMock = new Mock(); + datafileService = datafileServiceMock.Object; configMock = jasmine.createSpyObj('configMock', ['getDomain']); fileReaderMock = new FileReader(); - dockerfileServiceImpl = new DockerfileServiceImpl(dataFileServiceMock, configMock, () => fileReaderMock); + dockerfileServiceImpl = new DockerfileServiceImpl(datafileService, configMock, () => fileReaderMock); }); describe("getDockerfile", () => { @@ -27,16 +27,15 @@ describe("DockerfileServiceImpl", () => { var forDataSpy: Spy; beforeEach(() => { - dataFileServiceMock.readDataArrayAsPossibleArchive.and.callFake((buf, success, failure) => { + datafileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { failure([]); }); - dataFileServiceMock.arrayToString.and.callFake((buf, callback) => { - var contents: string = ""; - callback(contents); + datafileServiceMock.setup(mock => mock.arrayToString).is((buf, callback) => { + callback(""); }); - dataFileServiceMock.blobToString.and.callFake((blob, callback) => { + datafileServiceMock.setup(mock => mock.blobToString).is((blob, callback) => { callback(blob.toString()); }); @@ -55,7 +54,7 @@ describe("DockerfileServiceImpl", () => { dockerfileServiceImpl.getDockerfile(file) .then((dockerfile: DockerfileInfoImpl) => { expect(readAsFileBufferSpy.calls.argsFor(0)[0]).toEqual(file); - expect(dataFileServiceMock.readDataArrayAsPossibleArchive).toHaveBeenCalled(); + expect(datafileService.readDataArrayAsPossibleArchive).toHaveBeenCalled(); done(); }) .catch((error: string) => { @@ -67,7 +66,7 @@ describe("DockerfileServiceImpl", () => { it("calls datafile service to convert file to string if given file is not an archive", (done) => { dockerfileServiceImpl.getDockerfile(file) .then((dockerfile: DockerfileInfoImpl) => { - expect(dataFileServiceMock.arrayToString.calls.argsFor(0)[0]).toEqual(file); + expect((datafileService.arrayToString).calls.argsFor(0)[0]).toEqual(file); done(); }) .catch((error: string) => { @@ -102,7 +101,7 @@ describe("DockerfileServiceImpl", () => { }); it("returns rejected promise if given archive file with no Dockerfile present in root directory", (done) => { - dataFileServiceMock.readDataArrayAsPossibleArchive.and.callFake((buf, success, failure) => { + datafileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { success(invalidArchiveFile); }); @@ -118,14 +117,14 @@ describe("DockerfileServiceImpl", () => { }); it("calls datafile service to convert blob to string if given file is an archive", (done) => { - dataFileServiceMock.readDataArrayAsPossibleArchive.and.callFake((buf, success, failure) => { + datafileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { success(validArchiveFile); }); dockerfileServiceImpl.getDockerfile(file) .then((dockerfile: DockerfileInfoImpl) => { expect(validArchiveFile[0].toBlob).toHaveBeenCalled(); - expect(dataFileServiceMock.blobToString.calls.argsFor(0)[0]).toEqual(validArchiveFile[0].toBlob()); + expect((datafileService.blobToString).calls.argsFor(0)[0]).toEqual(validArchiveFile[0].toBlob()); done(); }) .catch((error: string) => { @@ -137,7 +136,7 @@ describe("DockerfileServiceImpl", () => { it("returns rejected promise if given archive file with invalid Dockerfile", (done) => { forDataSpy.and.returnValue(null); invalidArchiveFile[0].name = 'Dockerfile'; - dataFileServiceMock.readDataArrayAsPossibleArchive.and.callFake((buf, success, failure) => { + datafileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { success(invalidArchiveFile); }); @@ -153,7 +152,7 @@ describe("DockerfileServiceImpl", () => { }); it("returns resolved promise of new DockerfileInfoImpl instance if given archive with valid Dockerfile", (done) => { - dataFileServiceMock.readDataArrayAsPossibleArchive.and.callFake((buf, success, failure) => { + datafileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { success(validArchiveFile); }); diff --git a/static/js/services/dockerfile/dockerfile.service.impl.ts b/static/js/services/dockerfile/dockerfile.service.impl.ts index e2826b54c..a6e53be81 100644 --- a/static/js/services/dockerfile/dockerfile.service.impl.ts +++ b/static/js/services/dockerfile/dockerfile.service.impl.ts @@ -1,11 +1,12 @@ import { DockerfileService, DockerfileInfo } from './dockerfile.service'; import { Injectable } from 'angular-ts-decorators'; +import { DatafileService } from '../datafile/datafile.service'; @Injectable(DockerfileService.name) export class DockerfileServiceImpl implements DockerfileService { - constructor(private DataFileService: any, + constructor(private DataFileService: DatafileService, private Config: any, private fileReaderFactory: () => FileReader) { diff --git a/yarn.lock b/yarn.lock index 2d7325650..bfeb362ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3880,6 +3880,10 @@ ts-loader@^0.9.5: object-assign "^4.1.0" semver "^5.0.1" +ts-mocks@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/ts-mocks/-/ts-mocks-0.2.2.tgz#051e5b3a30068f6b9f1b1faa552a6f172793c6d6" + ts-node@^1.2.1: version "1.7.3" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-1.7.3.tgz#dee7f8a84751732d3c2e497cac5a02fb117dfee7" From b72cf7c04bd9fc31b4ef4537eba563a3abf54827 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Sat, 11 Mar 2017 15:47:44 -0800 Subject: [PATCH 09/14] refactoring DatafileService --- .../datafile/datafile.service.impl.spec.ts | 26 ++++ .../datafile/datafile.service.impl.ts | 134 ++++++++++++++++++ 2 files changed, 160 insertions(+) diff --git a/static/js/services/datafile/datafile.service.impl.spec.ts b/static/js/services/datafile/datafile.service.impl.spec.ts index e69de29bb..afdd14f6c 100644 --- a/static/js/services/datafile/datafile.service.impl.spec.ts +++ b/static/js/services/datafile/datafile.service.impl.spec.ts @@ -0,0 +1,26 @@ +import { DatafileServiceImpl } from './datafile.service.impl'; + + +describe("DatafileServiceImpl", () => { + var datafileServiceImpl: DatafileServiceImpl; + + beforeEach(() => { + datafileServiceImpl = new DatafileServiceImpl(); + }); + + describe("blobToString", () => { + + }); + + describe("arrayToString", () => { + + }); + + describe("readDataArrayAsPossibleArchive", () => { + + }); + + describe("downloadDataFileAsArrayBuffer", () => { + + }); +}); \ No newline at end of file diff --git a/static/js/services/datafile/datafile.service.impl.ts b/static/js/services/datafile/datafile.service.impl.ts index e69de29bb..14e611a73 100644 --- a/static/js/services/datafile/datafile.service.impl.ts +++ b/static/js/services/datafile/datafile.service.impl.ts @@ -0,0 +1,134 @@ +import { DatafileService } from './datafile.service'; +declare const JSZip: (buf: any) => void; +declare const Zlib: any; +declare const Untar: (uint8Array: Uint8Array) => void; + + +export class DatafileServiceImpl implements DatafileService { + + public blobToString(blob: any, callback: (result: string) => void): void { + + } + + public arrayToString(buf: any, callback: (result: string) => void): void { + + } + + public readDataArrayAsPossibleArchive(buf: any, + success: (result: any) => void, + failure: (error: any) => void): void { + + } + + public downloadDataFileAsArrayBuffer($scope: ng.IScope, + url: string, + progress: (percent: number) => void, + error: () => void, + loaded: (uint8array: Uint8Array) => void): void { + + } + + private getName(filePath: string): string { + var parts: string[] = filePath.split('/'); + + return parts[parts.length - 1]; + } + + private tryAsZip(buf: any, success: (result: any) => void, failure: (error?: any) => void): void { + var zip = null; + var zipFiles = null; + try { + var zip = new JSZip(buf); + zipFiles = zip.files; + } catch (e) { + failure(); + return; + } + + var files = []; + for (var filePath in zipFiles) { + if (zipFiles.hasOwnProperty(filePath)) { + files.push({ + 'name': this.getName(filePath), + 'path': filePath, + 'canRead': true, + 'toBlob': (function(fp) { + return function() { + return new Blob([zip.file(fp).asArrayBuffer()]); + }; + }(filePath)) + }); + } + } + + success(files); + } + + private tryAsTarGz(buf: any, success: (result: any) => void, failure: (error?: any) => void): void { + var gunzip = new Zlib.Gunzip(new Uint8Array(buf)); + var plain = null; + + try { + plain = gunzip.decompress(); + } catch (e) { + failure(); + return; + } + + if (plain.byteLength == 0) { + plain = buf; + } + + this.tryAsTar(plain, success, failure); + } + + private tryAsTar(buf: any, success: (result: any) => void, failure: (error?: any) => void): void { + var collapsePath = function(originalPath) { + // Tar files can contain entries of the form './', so we need to collapse + // those paths down. + var parts = originalPath.split('/'); + for (var i = parts.length - 1; i >= 0; i--) { + var part = parts[i]; + if (part == '.') { + parts.splice(i, 1); + } + } + return parts.join('/'); + }; + + try { + var handler = new Untar(new Uint8Array(buf)); + handler.process((status, read, files, err) => { + switch (status) { + case 'error': + failure(err); + break; + + case 'done': + var processed = []; + for (var i = 0; i < files.length; ++i) { + var currentFile = files[i]; + var path = collapsePath(currentFile.meta.filename); + + if (path == '' || path == 'pax_global_header') { continue; } + + processed.push({ + 'name': this.getName(path), + 'path': path, + 'canRead': true, + 'toBlob': (function(currentFile) { + return function() { + return new Blob([currentFile.buffer], {type: 'application/octet-binary'}); + }; + }(currentFile)) + }); + } + success(processed); + break; + } + }); + } catch (e) { + failure(); + } + } +} \ No newline at end of file From ade4216642e1bfa6289f62622ebe418ccfe432fe Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Sat, 11 Mar 2017 16:48:05 -0800 Subject: [PATCH 10/14] build single test bunding using Webpack context --- karma.conf.js | 6 +-- static/js/quay.module.ts | 2 + .../datafile/datafile.service.impl.spec.ts | 8 +-- .../datafile/datafile.service.impl.ts | 52 +++++++++++++++++-- .../js/services/datafile/datafile.service.ts | 2 +- .../dockerfile.service.impl.spec.ts | 32 ++++++------ .../dockerfile/dockerfile.service.impl.ts | 4 +- static/test/test-index.ts | 6 +++ 8 files changed, 81 insertions(+), 31 deletions(-) create mode 100644 static/test/test-index.ts diff --git a/karma.conf.js b/karma.conf.js index 38d9cd46e..220ed46e0 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -30,8 +30,8 @@ module.exports = function(config) { // static/lib resources 'static/lib/**/*.js', - // Tests - 'static/js/**/*.spec.ts*', + // Single entrypoint for all tests + 'static/test/test-index.ts', // Tests utils 'static/test/**/*.js', @@ -41,7 +41,7 @@ module.exports = function(config) { 'static/lib/ngReact/react.ngReact.min.js': ['webpack'], 'static/lib/angular-moment.min.js': ['webpack'], 'node_modules/core-js/index.js': ['webpack'], - 'static/js/**/*.spec.ts*': ['webpack'], + 'static/test/test-index.ts': ['webpack'], }, webpack: webpackConfig, webpackMiddleware: { diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index 47bb4954f..a456e4e89 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -16,6 +16,7 @@ import { QuayRun } from './quay-run.module'; import { BuildServiceImpl } from './services/build/build.service.impl'; import { AvatarServiceImpl } from './services/avatar/avatar.service.impl'; import { DockerfileServiceImpl } from './services/dockerfile/dockerfile.service.impl'; +import { DataFileServiceImpl } from './services/datafile/datafile.service.impl'; /** @@ -40,6 +41,7 @@ import { DockerfileServiceImpl } from './services/dockerfile/dockerfile.service. BuildServiceImpl, AvatarServiceImpl, DockerfileServiceImpl, + DataFileServiceImpl, ], }) export class quay { diff --git a/static/js/services/datafile/datafile.service.impl.spec.ts b/static/js/services/datafile/datafile.service.impl.spec.ts index afdd14f6c..b329f7555 100644 --- a/static/js/services/datafile/datafile.service.impl.spec.ts +++ b/static/js/services/datafile/datafile.service.impl.spec.ts @@ -1,11 +1,11 @@ -import { DatafileServiceImpl } from './datafile.service.impl'; +import { DataFileServiceImpl } from './datafile.service.impl'; -describe("DatafileServiceImpl", () => { - var datafileServiceImpl: DatafileServiceImpl; +describe("DataFileServiceImpl", () => { + var dataFileServiceImpl: DataFileServiceImpl; beforeEach(() => { - datafileServiceImpl = new DatafileServiceImpl(); + dataFileServiceImpl = new DataFileServiceImpl(); }); describe("blobToString", () => { diff --git a/static/js/services/datafile/datafile.service.impl.ts b/static/js/services/datafile/datafile.service.impl.ts index 14e611a73..93bbf14d6 100644 --- a/static/js/services/datafile/datafile.service.impl.ts +++ b/static/js/services/datafile/datafile.service.impl.ts @@ -1,23 +1,36 @@ -import { DatafileService } from './datafile.service'; +import { DataFileService } from './datafile.service'; +import { Injectable } from 'angular-ts-decorators'; declare const JSZip: (buf: any) => void; declare const Zlib: any; declare const Untar: (uint8Array: Uint8Array) => void; -export class DatafileServiceImpl implements DatafileService { +@Injectable(DataFileService.name) +export class DataFileServiceImpl implements DataFileService { public blobToString(blob: any, callback: (result: string) => void): void { - + var reader: FileReader = new FileReader(); + reader.onload = (event: Event) => callback(reader.result); + reader.readAsText(blob); } public arrayToString(buf: any, callback: (result: string) => void): void { - + var blob: Blob = new Blob([buf], {type: 'application/octet-binary'}); + var reader = new FileReader(); + reader.onload = (event: Event) => callback(event.target['result']); + reader.onerror = (event: Event) => callback(null); + reader.onabort = (event: Event) => callback(null); + reader.readAsText(blob); } public readDataArrayAsPossibleArchive(buf: any, success: (result: any) => void, failure: (error: any) => void): void { - + this.tryAsZip(buf, success, () => { + this.tryAsTarGz(buf, success, () => { + this.tryAsTar(buf, success, failure); + }); + }); } public downloadDataFileAsArrayBuffer($scope: ng.IScope, @@ -25,7 +38,36 @@ export class DatafileServiceImpl implements DatafileService { progress: (percent: number) => void, error: () => void, loaded: (uint8array: Uint8Array) => void): void { + var request: XMLHttpRequest = new XMLHttpRequest(); + request.open('GET', url, true); + request.responseType = 'arraybuffer'; + request.onprogress = (e) => { + $scope.$apply(() => { + var percentLoaded; + if (e.lengthComputable) { + progress(e.loaded / e.total); + } + }); + }; + + request.onerror = () => { + $scope.$apply(() => { + error(); + }); + }; + + request.onload = function() { + if (request.status == 200) { + $scope.$apply(() => { + var uint8array = new Uint8Array(request.response); + loaded(uint8array); + }); + return; + } + }; + + request.send(); } private getName(filePath: string): string { diff --git a/static/js/services/datafile/datafile.service.ts b/static/js/services/datafile/datafile.service.ts index 43bae9d11..cd7bf93ca 100644 --- a/static/js/services/datafile/datafile.service.ts +++ b/static/js/services/datafile/datafile.service.ts @@ -6,7 +6,7 @@ * - Blob.js * - zlib.js */ -export abstract class DatafileService { +export abstract class DataFileService { /** * Convert a blob to a string. diff --git a/static/js/services/dockerfile/dockerfile.service.impl.spec.ts b/static/js/services/dockerfile/dockerfile.service.impl.spec.ts index ebccbc2ea..bd6df347d 100644 --- a/static/js/services/dockerfile/dockerfile.service.impl.spec.ts +++ b/static/js/services/dockerfile/dockerfile.service.impl.spec.ts @@ -1,22 +1,22 @@ import { DockerfileServiceImpl, DockerfileInfoImpl } from './dockerfile.service.impl'; -import { DatafileService } from '../datafile/datafile.service'; +import { DataFileService } from '../datafile/datafile.service'; import Spy = jasmine.Spy; import { Mock } from 'ts-mocks'; describe("DockerfileServiceImpl", () => { var dockerfileServiceImpl: DockerfileServiceImpl; - var datafileServiceMock: Mock; - var datafileService: DatafileService; + var dataFileServiceMock: Mock; + var dataFileService: DataFileService; var configMock: any; var fileReaderMock: FileReader; beforeEach(() => { - datafileServiceMock = new Mock(); - datafileService = datafileServiceMock.Object; + dataFileServiceMock = new Mock(); + dataFileService = dataFileServiceMock.Object; configMock = jasmine.createSpyObj('configMock', ['getDomain']); fileReaderMock = new FileReader(); - dockerfileServiceImpl = new DockerfileServiceImpl(datafileService, configMock, () => fileReaderMock); + dockerfileServiceImpl = new DockerfileServiceImpl(dataFileService, configMock, () => fileReaderMock); }); describe("getDockerfile", () => { @@ -27,15 +27,15 @@ describe("DockerfileServiceImpl", () => { var forDataSpy: Spy; beforeEach(() => { - datafileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { + dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { failure([]); }); - datafileServiceMock.setup(mock => mock.arrayToString).is((buf, callback) => { + dataFileServiceMock.setup(mock => mock.arrayToString).is((buf, callback) => { callback(""); }); - datafileServiceMock.setup(mock => mock.blobToString).is((blob, callback) => { + dataFileServiceMock.setup(mock => mock.blobToString).is((blob, callback) => { callback(blob.toString()); }); @@ -54,7 +54,7 @@ describe("DockerfileServiceImpl", () => { dockerfileServiceImpl.getDockerfile(file) .then((dockerfile: DockerfileInfoImpl) => { expect(readAsFileBufferSpy.calls.argsFor(0)[0]).toEqual(file); - expect(datafileService.readDataArrayAsPossibleArchive).toHaveBeenCalled(); + expect(dataFileService.readDataArrayAsPossibleArchive).toHaveBeenCalled(); done(); }) .catch((error: string) => { @@ -66,7 +66,7 @@ describe("DockerfileServiceImpl", () => { it("calls datafile service to convert file to string if given file is not an archive", (done) => { dockerfileServiceImpl.getDockerfile(file) .then((dockerfile: DockerfileInfoImpl) => { - expect((datafileService.arrayToString).calls.argsFor(0)[0]).toEqual(file); + expect((dataFileService.arrayToString).calls.argsFor(0)[0]).toEqual(file); done(); }) .catch((error: string) => { @@ -101,7 +101,7 @@ describe("DockerfileServiceImpl", () => { }); it("returns rejected promise if given archive file with no Dockerfile present in root directory", (done) => { - datafileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { + dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { success(invalidArchiveFile); }); @@ -117,14 +117,14 @@ describe("DockerfileServiceImpl", () => { }); it("calls datafile service to convert blob to string if given file is an archive", (done) => { - datafileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { + dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { success(validArchiveFile); }); dockerfileServiceImpl.getDockerfile(file) .then((dockerfile: DockerfileInfoImpl) => { expect(validArchiveFile[0].toBlob).toHaveBeenCalled(); - expect((datafileService.blobToString).calls.argsFor(0)[0]).toEqual(validArchiveFile[0].toBlob()); + expect((dataFileService.blobToString).calls.argsFor(0)[0]).toEqual(validArchiveFile[0].toBlob()); done(); }) .catch((error: string) => { @@ -136,7 +136,7 @@ describe("DockerfileServiceImpl", () => { it("returns rejected promise if given archive file with invalid Dockerfile", (done) => { forDataSpy.and.returnValue(null); invalidArchiveFile[0].name = 'Dockerfile'; - datafileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { + dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { success(invalidArchiveFile); }); @@ -152,7 +152,7 @@ describe("DockerfileServiceImpl", () => { }); it("returns resolved promise of new DockerfileInfoImpl instance if given archive with valid Dockerfile", (done) => { - datafileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { + dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { success(validArchiveFile); }); diff --git a/static/js/services/dockerfile/dockerfile.service.impl.ts b/static/js/services/dockerfile/dockerfile.service.impl.ts index a6e53be81..54adc51c8 100644 --- a/static/js/services/dockerfile/dockerfile.service.impl.ts +++ b/static/js/services/dockerfile/dockerfile.service.impl.ts @@ -1,12 +1,12 @@ import { DockerfileService, DockerfileInfo } from './dockerfile.service'; import { Injectable } from 'angular-ts-decorators'; -import { DatafileService } from '../datafile/datafile.service'; +import { DataFileService } from '../datafile/datafile.service'; @Injectable(DockerfileService.name) export class DockerfileServiceImpl implements DockerfileService { - constructor(private DataFileService: DatafileService, + constructor(private DataFileService: DataFileService, private Config: any, private fileReaderFactory: () => FileReader) { diff --git a/static/test/test-index.ts b/static/test/test-index.ts new file mode 100644 index 000000000..5a30fd447 --- /dev/null +++ b/static/test/test-index.ts @@ -0,0 +1,6 @@ +declare var require: any; + + +// Require all modules ending in ".spec.ts" from the js directory and all subdirectories +var testsContext = require.context("../js", true, /\.spec\.ts$/); +testsContext.keys().forEach(testsContext); From 7416534ab8da6542ee26cb2f65fc7ae060ceb6a0 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Sat, 11 Mar 2017 17:45:36 -0800 Subject: [PATCH 11/14] better mocking of FileReader and events --- .../datafile/datafile.service.impl.spec.ts | 18 ++++++++++- .../datafile/datafile.service.impl.ts | 6 +++- .../dockerfile.service.impl.spec.ts | 31 ++++++++----------- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/static/js/services/datafile/datafile.service.impl.spec.ts b/static/js/services/datafile/datafile.service.impl.spec.ts index b329f7555..7f828c750 100644 --- a/static/js/services/datafile/datafile.service.impl.spec.ts +++ b/static/js/services/datafile/datafile.service.impl.spec.ts @@ -1,19 +1,35 @@ import { DataFileServiceImpl } from './datafile.service.impl'; +import { Mock } from 'ts-mocks'; describe("DataFileServiceImpl", () => { var dataFileServiceImpl: DataFileServiceImpl; + var fileReaderMock: Mock; + var fileReader: FileReader; beforeEach(() => { - dataFileServiceImpl = new DataFileServiceImpl(); + fileReaderMock = new Mock(); + fileReader = fileReaderMock.Object; + dataFileServiceImpl = new DataFileServiceImpl(() => fileReader); }); describe("blobToString", () => { + beforeEach(() => { + fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => { + fileReaderMock.Object.onload({target: {result: blob.toString()}}); + }); + }); + }); describe("arrayToString", () => { + beforeEach(() => { + fileReaderMock.setup(mock => mock.readAsArrayBuffer).is((blob: Blob) => { + fileReaderMock.Object.onload({target: {result: blob.toString()}}); + }); + }); }); describe("readDataArrayAsPossibleArchive", () => { diff --git a/static/js/services/datafile/datafile.service.impl.ts b/static/js/services/datafile/datafile.service.impl.ts index 93bbf14d6..ed0e50fec 100644 --- a/static/js/services/datafile/datafile.service.impl.ts +++ b/static/js/services/datafile/datafile.service.impl.ts @@ -8,9 +8,13 @@ declare const Untar: (uint8Array: Uint8Array) => void; @Injectable(DataFileService.name) export class DataFileServiceImpl implements DataFileService { + constructor(private fileReaderFactory: () => FileReader) { + + } + public blobToString(blob: any, callback: (result: string) => void): void { var reader: FileReader = new FileReader(); - reader.onload = (event: Event) => callback(reader.result); + reader.onload = (event: Event) => callback(event.target['result']); reader.readAsText(blob); } diff --git a/static/js/services/dockerfile/dockerfile.service.impl.spec.ts b/static/js/services/dockerfile/dockerfile.service.impl.spec.ts index bd6df347d..01eefe3e2 100644 --- a/static/js/services/dockerfile/dockerfile.service.impl.spec.ts +++ b/static/js/services/dockerfile/dockerfile.service.impl.spec.ts @@ -9,51 +9,46 @@ describe("DockerfileServiceImpl", () => { var dataFileServiceMock: Mock; var dataFileService: DataFileService; var configMock: any; - var fileReaderMock: FileReader; + var fileReaderMock: Mock; beforeEach(() => { dataFileServiceMock = new Mock(); dataFileService = dataFileServiceMock.Object; configMock = jasmine.createSpyObj('configMock', ['getDomain']); - fileReaderMock = new FileReader(); - dockerfileServiceImpl = new DockerfileServiceImpl(dataFileService, configMock, () => fileReaderMock); + fileReaderMock = new Mock(); + dockerfileServiceImpl = new DockerfileServiceImpl(dataFileService, configMock, () => fileReaderMock.Object); }); describe("getDockerfile", () => { var file: any; var invalidArchiveFile: any[]; var validArchiveFile: any[]; - var readAsFileBufferSpy: Spy; var forDataSpy: Spy; beforeEach(() => { + file = "FROM quay.io/coreos/nginx:latest"; + validArchiveFile = [{name: 'Dockerfile', toBlob: jasmine.createSpy('toBlobSpy').and.returnValue(file)}]; + invalidArchiveFile = [{name: 'main.exe', toBlob: jasmine.createSpy('toBlobSpy').and.returnValue("")}]; + dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => { failure([]); }); - dataFileServiceMock.setup(mock => mock.arrayToString).is((buf, callback) => { - callback(""); - }); + dataFileServiceMock.setup(mock => mock.arrayToString).is((buf, callback) => callback("")); - dataFileServiceMock.setup(mock => mock.blobToString).is((blob, callback) => { - callback(blob.toString()); - }); + dataFileServiceMock.setup(mock => mock.blobToString).is((blob, callback) => callback(blob.toString())); forDataSpy = spyOn(DockerfileInfoImpl, "forData").and.returnValue(new DockerfileInfoImpl(file, configMock)); - readAsFileBufferSpy = spyOn(fileReaderMock, "readAsArrayBuffer").and.callFake(() => { - var event: any = {target: {result: file}}; - fileReaderMock.onload(event); - }); - file = "FROM quay.io/coreos/nginx:latest"; - validArchiveFile = [{name: 'Dockerfile', toBlob: jasmine.createSpy('toBlobSpy').and.returnValue(file)}]; - invalidArchiveFile = [{name: 'main.exe', toBlob: jasmine.createSpy('toBlobSpy').and.returnValue("")}]; + fileReaderMock.setup(mock => mock.readAsArrayBuffer).is((blob: Blob) => { + fileReaderMock.Object.onload({target: {result: file}}); + }); }); it("calls datafile service to read given file as possible archive file", (done) => { dockerfileServiceImpl.getDockerfile(file) .then((dockerfile: DockerfileInfoImpl) => { - expect(readAsFileBufferSpy.calls.argsFor(0)[0]).toEqual(file); + expect((fileReaderMock.Object.readAsArrayBuffer).calls.argsFor(0)[0]).toEqual(file); expect(dataFileService.readDataArrayAsPossibleArchive).toHaveBeenCalled(); done(); }) From 527e108d2ef8dfb11738080e1fb49c8243611c0b Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Sun, 12 Mar 2017 00:21:59 -0800 Subject: [PATCH 12/14] tests for DataFileServiceImpl --- .../datafile/datafile.service.impl.spec.ts | 88 ++++++++++++++++++- .../datafile/datafile.service.impl.ts | 10 ++- .../js/services/datafile/datafile.service.ts | 2 +- 3 files changed, 92 insertions(+), 8 deletions(-) diff --git a/static/js/services/datafile/datafile.service.impl.spec.ts b/static/js/services/datafile/datafile.service.impl.spec.ts index 7f828c750..33e3a1f29 100644 --- a/static/js/services/datafile/datafile.service.impl.spec.ts +++ b/static/js/services/datafile/datafile.service.impl.spec.ts @@ -1,5 +1,6 @@ import { DataFileServiceImpl } from './datafile.service.impl'; import { Mock } from 'ts-mocks'; +import Spy = jasmine.Spy; describe("DataFileServiceImpl", () => { @@ -14,20 +15,101 @@ describe("DataFileServiceImpl", () => { }); describe("blobToString", () => { + var data: any; + var blob: Blob; beforeEach(() => { + data = {hello: "world"}; + blob = new Blob([JSON.stringify(data)]); + fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => { - fileReaderMock.Object.onload({target: {result: blob.toString()}}); + fileReaderMock.Object.onload({target: {result: data}}); }); }); + it("calls file reader to read given blob", (done) => { + dataFileServiceImpl.blobToString(blob, (result) => { + expect((fileReader.readAsText).calls.argsFor(0)[0]).toEqual(blob); + done(); + }); + }); + + it("calls given callback with null if file reader errors", (done) => { + fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => { + fileReaderMock.Object.onerror(new ErrorEvent("onerror")); + }); + + dataFileServiceImpl.blobToString(blob, (result) => { + expect(result).toBe(null); + done(); + }); + }); + + it("calls given callback with null if file reader aborts", (done) => { + fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => { + fileReaderMock.Object.onabort(new Event("onabort")); + }); + + dataFileServiceImpl.blobToString(blob, (result) => { + expect(result).toBe(null); + done(); + }); + }); + + it("calls given callback with result when file reader successfully loads", (done) => { + dataFileServiceImpl.blobToString(blob, (result) => { + expect(result).toBe(data); + done(); + }); + }); }); describe("arrayToString", () => { + var blob: Blob; + var data: any; beforeEach(() => { - fileReaderMock.setup(mock => mock.readAsArrayBuffer).is((blob: Blob) => { - fileReaderMock.Object.onload({target: {result: blob.toString()}}); + data = JSON.stringify({hello: "world"}); + blob = new Blob([data], {type: 'application/octet-binary'}); + + fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => { + fileReaderMock.Object.onload({target: {result: data}}); + }); + }); + + it("calls file reader to read blob created from given buffer", (done) => { + dataFileServiceImpl.arrayToString(data, (result) => { + expect((fileReader.readAsText).calls.argsFor(0)[0]).toEqual(blob); + done(); + }); + }); + + it("calls given callback with null if file reader errors", (done) => { + fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => { + fileReaderMock.Object.onerror(new ErrorEvent("onerror")); + }); + + dataFileServiceImpl.arrayToString(data, (result) => { + expect(result).toEqual(null); + done(); + }); + }); + + it("calls given callback with null if file reader aborts", (done) => { + fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => { + fileReaderMock.Object.onabort(new Event("onabort")); + }); + + dataFileServiceImpl.arrayToString(data, (result) => { + expect(result).toEqual(null); + done(); + }); + }); + + it("calls given callback with result when file reader successfully loads", (done) => { + dataFileServiceImpl.arrayToString(data, (result) => { + expect(result).toEqual(data); + done(); }); }); }); diff --git a/static/js/services/datafile/datafile.service.impl.ts b/static/js/services/datafile/datafile.service.impl.ts index ed0e50fec..be5ce180f 100644 --- a/static/js/services/datafile/datafile.service.impl.ts +++ b/static/js/services/datafile/datafile.service.impl.ts @@ -12,15 +12,17 @@ export class DataFileServiceImpl implements DataFileService { } - public blobToString(blob: any, callback: (result: string) => void): void { - var reader: FileReader = new FileReader(); + public blobToString(blob: Blob, callback: (result: string) => void): void { + var reader: FileReader = this.fileReaderFactory(); reader.onload = (event: Event) => callback(event.target['result']); + reader.onerror = (event: Event) => callback(null); + reader.onabort = (event: Event) => callback(null); reader.readAsText(blob); } public arrayToString(buf: any, callback: (result: string) => void): void { - var blob: Blob = new Blob([buf], {type: 'application/octet-binary'}); - var reader = new FileReader(); + const blob: Blob = new Blob([buf], {type: 'application/octet-binary'}); + var reader: FileReader = this.fileReaderFactory(); reader.onload = (event: Event) => callback(event.target['result']); reader.onerror = (event: Event) => callback(null); reader.onabort = (event: Event) => callback(null); diff --git a/static/js/services/datafile/datafile.service.ts b/static/js/services/datafile/datafile.service.ts index cd7bf93ca..47c990d83 100644 --- a/static/js/services/datafile/datafile.service.ts +++ b/static/js/services/datafile/datafile.service.ts @@ -13,7 +13,7 @@ export abstract class DataFileService { * @param blob The blob to convert. * @param callback The success callback given converted blob. */ - public abstract blobToString(blob: any, callback: (result: string) => void): void; + public abstract blobToString(blob: Blob, callback: (result: string) => void): void; /** * Convert array to string. From 11425191390321c1aebe5910e68199ba45cb93ce Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Sun, 12 Mar 2017 00:29:39 -0800 Subject: [PATCH 13/14] tests for DataFileServiceImpl --- static/js/services/datafile-service.js | 362 +++++++++--------- .../datafile/datafile.service.impl.spec.ts | 6 +- .../datafile/datafile.service.impl.ts | 2 +- 3 files changed, 185 insertions(+), 185 deletions(-) diff --git a/static/js/services/datafile-service.js b/static/js/services/datafile-service.js index 3b8368076..73878ab0d 100644 --- a/static/js/services/datafile-service.js +++ b/static/js/services/datafile-service.js @@ -1,181 +1,181 @@ -/** - * Service which provides helper methods for downloading a data file from a URL, and extracting - * its contents as .tar, .tar.gz, or .zip file. Note that this service depends on external - * library code in the lib/ directory: - * - jszip.min.js - * - Blob.js - * - zlib.js - */ -angular.module('quay').factory('DataFileService', [function() { - var dataFileService = {}; - - dataFileService.getName_ = function(filePath) { - var parts = filePath.split('/'); - return parts[parts.length - 1]; - }; - - dataFileService.tryAsZip_ = function(buf, success, failure) { - var zip = null; - var zipFiles = null; - try { - var zip = new JSZip(buf); - zipFiles = zip.files; - } catch (e) { - failure(); - return; - } - - var files = []; - for (var filePath in zipFiles) { - if (zipFiles.hasOwnProperty(filePath)) { - files.push({ - 'name': dataFileService.getName_(filePath), - 'path': filePath, - 'canRead': true, - 'toBlob': (function(fp) { - return function() { - return new Blob([zip.file(fp).asArrayBuffer()]); - }; - }(filePath)) - }); - } - } - - success(files); - }; - - dataFileService.tryAsTarGz_ = function(buf, success, failure) { - var gunzip = new Zlib.Gunzip(new Uint8Array(buf)); - var plain = null; - - try { - plain = gunzip.decompress(); - } catch (e) { - failure(); - return; - } - - if (plain.byteLength == 0) { - plain = buf; - } - - dataFileService.tryAsTar_(plain, success, failure); - }; - - dataFileService.tryAsTar_ = function(buf, success, failure) { - var collapsePath = function(originalPath) { - // Tar files can contain entries of the form './', so we need to collapse - // those paths down. - var parts = originalPath.split('/'); - for (var i = parts.length - 1; i >= 0; i--) { - var part = parts[i]; - if (part == '.') { - parts.splice(i, 1); - } - } - return parts.join('/'); - }; - - try { - var handler = new Untar(new Uint8Array(buf)); - handler.process(function(status, read, files, err) { - switch (status) { - case 'error': - failure(err); - break; - - case 'done': - var processed = []; - for (var i = 0; i < files.length; ++i) { - var currentFile = files[i]; - var path = collapsePath(currentFile.meta.filename); - - if (path == '' || path == 'pax_global_header') { continue; } - - processed.push({ - 'name': dataFileService.getName_(path), - 'path': path, - 'canRead': true, - 'toBlob': (function(currentFile) { - return function() { - return new Blob([currentFile.buffer], {type: 'application/octet-binary'}); - }; - }(currentFile)) - }); - } - success(processed); - break; - } - }); - } catch (e) { - failure(); - } - - }; - - dataFileService.blobToString = function(blob, callback) { - var reader = new FileReader(); - reader.onload = function(event){ - callback(reader.result); - }; - reader.readAsText(blob); - }; - - dataFileService.arrayToString = function(buf, callback) { - var bb = new Blob([buf], {type: 'application/octet-binary'}); - var f = new FileReader(); - f.onload = function(e) { - callback(e.target.result); - }; - f.onerror = function(e) { - callback(null); - }; - f.onabort = function(e) { - callback(null); - }; - f.readAsText(bb); - }; - - dataFileService.readDataArrayAsPossibleArchive = function(buf, success, failure) { - dataFileService.tryAsZip_(buf, success, function() { - dataFileService.tryAsTarGz_(buf, success, function() { - dataFileService.tryAsTar_(buf, success, failure); - }); - }); - }; - - dataFileService.downloadDataFileAsArrayBuffer = function($scope, url, progress, error, loaded) { - var request = new XMLHttpRequest(); - request.open('GET', url, true); - request.responseType = 'arraybuffer'; - - request.onprogress = function(e) { - $scope.$apply(function() { - var percentLoaded; - if (e.lengthComputable) { - progress(e.loaded / e.total); - } - }); - }; - - request.onerror = function() { - $scope.$apply(function() { - error(); - }); - }; - - request.onload = function() { - if (this.status == 200) { - $scope.$apply(function() { - var uint8array = new Uint8Array(request.response); - loaded(uint8array); - }); - return; - } - }; - - request.send(); - }; - - return dataFileService; -}]); +// /** +// * Service which provides helper methods for downloading a data file from a URL, and extracting +// * its contents as .tar, .tar.gz, or .zip file. Note that this service depends on external +// * library code in the lib/ directory: +// * - jszip.min.js +// * - Blob.js +// * - zlib.js +// */ +// angular.module('quay').factory('DataFileService', [function() { +// var dataFileService = {}; +// +// dataFileService.getName_ = function(filePath) { +// var parts = filePath.split('/'); +// return parts[parts.length - 1]; +// }; +// +// dataFileService.tryAsZip_ = function(buf, success, failure) { +// var zip = null; +// var zipFiles = null; +// try { +// var zip = new JSZip(buf); +// zipFiles = zip.files; +// } catch (e) { +// failure(); +// return; +// } +// +// var files = []; +// for (var filePath in zipFiles) { +// if (zipFiles.hasOwnProperty(filePath)) { +// files.push({ +// 'name': dataFileService.getName_(filePath), +// 'path': filePath, +// 'canRead': true, +// 'toBlob': (function(fp) { +// return function() { +// return new Blob([zip.file(fp).asArrayBuffer()]); +// }; +// }(filePath)) +// }); +// } +// } +// +// success(files); +// }; +// +// dataFileService.tryAsTarGz_ = function(buf, success, failure) { +// var gunzip = new Zlib.Gunzip(new Uint8Array(buf)); +// var plain = null; +// +// try { +// plain = gunzip.decompress(); +// } catch (e) { +// failure(); +// return; +// } +// +// if (plain.byteLength == 0) { +// plain = buf; +// } +// +// dataFileService.tryAsTar_(plain, success, failure); +// }; +// +// dataFileService.tryAsTar_ = function(buf, success, failure) { +// var collapsePath = function(originalPath) { +// // Tar files can contain entries of the form './', so we need to collapse +// // those paths down. +// var parts = originalPath.split('/'); +// for (var i = parts.length - 1; i >= 0; i--) { +// var part = parts[i]; +// if (part == '.') { +// parts.splice(i, 1); +// } +// } +// return parts.join('/'); +// }; +// +// try { +// var handler = new Untar(new Uint8Array(buf)); +// handler.process(function(status, read, files, err) { +// switch (status) { +// case 'error': +// failure(err); +// break; +// +// case 'done': +// var processed = []; +// for (var i = 0; i < files.length; ++i) { +// var currentFile = files[i]; +// var path = collapsePath(currentFile.meta.filename); +// +// if (path == '' || path == 'pax_global_header') { continue; } +// +// processed.push({ +// 'name': dataFileService.getName_(path), +// 'path': path, +// 'canRead': true, +// 'toBlob': (function(currentFile) { +// return function() { +// return new Blob([currentFile.buffer], {type: 'application/octet-binary'}); +// }; +// }(currentFile)) +// }); +// } +// success(processed); +// break; +// } +// }); +// } catch (e) { +// failure(); +// } +// +// }; +// +// dataFileService.blobToString = function(blob, callback) { +// var reader = new FileReader(); +// reader.onload = function(event){ +// callback(reader.result); +// }; +// reader.readAsText(blob); +// }; +// +// dataFileService.arrayToString = function(buf, callback) { +// var bb = new Blob([buf], {type: 'application/octet-binary'}); +// var f = new FileReader(); +// f.onload = function(e) { +// callback(e.target.result); +// }; +// f.onerror = function(e) { +// callback(null); +// }; +// f.onabort = function(e) { +// callback(null); +// }; +// f.readAsText(bb); +// }; +// +// dataFileService.readDataArrayAsPossibleArchive = function(buf, success, failure) { +// dataFileService.tryAsZip_(buf, success, function() { +// dataFileService.tryAsTarGz_(buf, success, function() { +// dataFileService.tryAsTar_(buf, success, failure); +// }); +// }); +// }; +// +// dataFileService.downloadDataFileAsArrayBuffer = function($scope, url, progress, error, loaded) { +// var request = new XMLHttpRequest(); +// request.open('GET', url, true); +// request.responseType = 'arraybuffer'; +// +// request.onprogress = function(e) { +// $scope.$apply(function() { +// var percentLoaded; +// if (e.lengthComputable) { +// progress(e.loaded / e.total); +// } +// }); +// }; +// +// request.onerror = function() { +// $scope.$apply(function() { +// error(); +// }); +// }; +// +// request.onload = function() { +// if (this.status == 200) { +// $scope.$apply(function() { +// var uint8array = new Uint8Array(request.response); +// loaded(uint8array); +// }); +// return; +// } +// }; +// +// request.send(); +// }; +// +// return dataFileService; +// }]); diff --git a/static/js/services/datafile/datafile.service.impl.spec.ts b/static/js/services/datafile/datafile.service.impl.spec.ts index 33e3a1f29..62539a794 100644 --- a/static/js/services/datafile/datafile.service.impl.spec.ts +++ b/static/js/services/datafile/datafile.service.impl.spec.ts @@ -115,10 +115,10 @@ describe("DataFileServiceImpl", () => { }); describe("readDataArrayAsPossibleArchive", () => { - + // TODO }); describe("downloadDataFileAsArrayBuffer", () => { - + // TODO }); -}); \ No newline at end of file +}); diff --git a/static/js/services/datafile/datafile.service.impl.ts b/static/js/services/datafile/datafile.service.impl.ts index be5ce180f..20b04ccc5 100644 --- a/static/js/services/datafile/datafile.service.impl.ts +++ b/static/js/services/datafile/datafile.service.impl.ts @@ -9,7 +9,7 @@ declare const Untar: (uint8Array: Uint8Array) => void; export class DataFileServiceImpl implements DataFileService { constructor(private fileReaderFactory: () => FileReader) { - + } public blobToString(blob: Blob, callback: (result: string) => void): void { From 2e33c47573601ae2339523c538567dcdc8f97d69 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Sun, 12 Mar 2017 00:34:55 -0800 Subject: [PATCH 14/14] removed datafile-service.js --- static/js/services/datafile-service.js | 181 ------------------ .../datafile/datafile.service.impl.ts | 2 +- 2 files changed, 1 insertion(+), 182 deletions(-) delete mode 100644 static/js/services/datafile-service.js diff --git a/static/js/services/datafile-service.js b/static/js/services/datafile-service.js deleted file mode 100644 index 73878ab0d..000000000 --- a/static/js/services/datafile-service.js +++ /dev/null @@ -1,181 +0,0 @@ -// /** -// * Service which provides helper methods for downloading a data file from a URL, and extracting -// * its contents as .tar, .tar.gz, or .zip file. Note that this service depends on external -// * library code in the lib/ directory: -// * - jszip.min.js -// * - Blob.js -// * - zlib.js -// */ -// angular.module('quay').factory('DataFileService', [function() { -// var dataFileService = {}; -// -// dataFileService.getName_ = function(filePath) { -// var parts = filePath.split('/'); -// return parts[parts.length - 1]; -// }; -// -// dataFileService.tryAsZip_ = function(buf, success, failure) { -// var zip = null; -// var zipFiles = null; -// try { -// var zip = new JSZip(buf); -// zipFiles = zip.files; -// } catch (e) { -// failure(); -// return; -// } -// -// var files = []; -// for (var filePath in zipFiles) { -// if (zipFiles.hasOwnProperty(filePath)) { -// files.push({ -// 'name': dataFileService.getName_(filePath), -// 'path': filePath, -// 'canRead': true, -// 'toBlob': (function(fp) { -// return function() { -// return new Blob([zip.file(fp).asArrayBuffer()]); -// }; -// }(filePath)) -// }); -// } -// } -// -// success(files); -// }; -// -// dataFileService.tryAsTarGz_ = function(buf, success, failure) { -// var gunzip = new Zlib.Gunzip(new Uint8Array(buf)); -// var plain = null; -// -// try { -// plain = gunzip.decompress(); -// } catch (e) { -// failure(); -// return; -// } -// -// if (plain.byteLength == 0) { -// plain = buf; -// } -// -// dataFileService.tryAsTar_(plain, success, failure); -// }; -// -// dataFileService.tryAsTar_ = function(buf, success, failure) { -// var collapsePath = function(originalPath) { -// // Tar files can contain entries of the form './', so we need to collapse -// // those paths down. -// var parts = originalPath.split('/'); -// for (var i = parts.length - 1; i >= 0; i--) { -// var part = parts[i]; -// if (part == '.') { -// parts.splice(i, 1); -// } -// } -// return parts.join('/'); -// }; -// -// try { -// var handler = new Untar(new Uint8Array(buf)); -// handler.process(function(status, read, files, err) { -// switch (status) { -// case 'error': -// failure(err); -// break; -// -// case 'done': -// var processed = []; -// for (var i = 0; i < files.length; ++i) { -// var currentFile = files[i]; -// var path = collapsePath(currentFile.meta.filename); -// -// if (path == '' || path == 'pax_global_header') { continue; } -// -// processed.push({ -// 'name': dataFileService.getName_(path), -// 'path': path, -// 'canRead': true, -// 'toBlob': (function(currentFile) { -// return function() { -// return new Blob([currentFile.buffer], {type: 'application/octet-binary'}); -// }; -// }(currentFile)) -// }); -// } -// success(processed); -// break; -// } -// }); -// } catch (e) { -// failure(); -// } -// -// }; -// -// dataFileService.blobToString = function(blob, callback) { -// var reader = new FileReader(); -// reader.onload = function(event){ -// callback(reader.result); -// }; -// reader.readAsText(blob); -// }; -// -// dataFileService.arrayToString = function(buf, callback) { -// var bb = new Blob([buf], {type: 'application/octet-binary'}); -// var f = new FileReader(); -// f.onload = function(e) { -// callback(e.target.result); -// }; -// f.onerror = function(e) { -// callback(null); -// }; -// f.onabort = function(e) { -// callback(null); -// }; -// f.readAsText(bb); -// }; -// -// dataFileService.readDataArrayAsPossibleArchive = function(buf, success, failure) { -// dataFileService.tryAsZip_(buf, success, function() { -// dataFileService.tryAsTarGz_(buf, success, function() { -// dataFileService.tryAsTar_(buf, success, failure); -// }); -// }); -// }; -// -// dataFileService.downloadDataFileAsArrayBuffer = function($scope, url, progress, error, loaded) { -// var request = new XMLHttpRequest(); -// request.open('GET', url, true); -// request.responseType = 'arraybuffer'; -// -// request.onprogress = function(e) { -// $scope.$apply(function() { -// var percentLoaded; -// if (e.lengthComputable) { -// progress(e.loaded / e.total); -// } -// }); -// }; -// -// request.onerror = function() { -// $scope.$apply(function() { -// error(); -// }); -// }; -// -// request.onload = function() { -// if (this.status == 200) { -// $scope.$apply(function() { -// var uint8array = new Uint8Array(request.response); -// loaded(uint8array); -// }); -// return; -// } -// }; -// -// request.send(); -// }; -// -// return dataFileService; -// }]); diff --git a/static/js/services/datafile/datafile.service.impl.ts b/static/js/services/datafile/datafile.service.impl.ts index 20b04ccc5..be5ce180f 100644 --- a/static/js/services/datafile/datafile.service.impl.ts +++ b/static/js/services/datafile/datafile.service.impl.ts @@ -9,7 +9,7 @@ declare const Untar: (uint8Array: Uint8Array) => void; export class DataFileServiceImpl implements DataFileService { constructor(private fileReaderFactory: () => FileReader) { - + } public blobToString(blob: Blob, callback: (result: string) => void): void {