Related discussion: https://github.com/angular/angular/pull/23576#discussion_r187925949. PR Close #25671
		
			
				
	
	
		
			145 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			145 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| // Imports
 | |
| import * as cp from 'child_process';
 | |
| import {EventEmitter} from 'events';
 | |
| import * as fs from 'fs';
 | |
| import * as path from 'path';
 | |
| import * as shell from 'shelljs';
 | |
| import {HIDDEN_DIR_PREFIX} from '../common/constants';
 | |
| import {assertNotMissingOrEmpty, computeShortSha, Logger} from '../common/utils';
 | |
| import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events';
 | |
| import {PreviewServerError} from './preview-error';
 | |
| 
 | |
| // Classes
 | |
| export class BuildCreator extends EventEmitter {
 | |
| 
 | |
|   private logger = new Logger('BuildCreator');
 | |
| 
 | |
|   // Constructor
 | |
|   constructor(protected buildsDir: string) {
 | |
|     super();
 | |
|     assertNotMissingOrEmpty('buildsDir', buildsDir);
 | |
|   }
 | |
| 
 | |
|   // Methods - Public
 | |
|   public create(pr: number, sha: string, archivePath: string, isPublic: boolean): Promise<void> {
 | |
|     // Use only part of the SHA for more readable URLs.
 | |
|     sha = computeShortSha(sha);
 | |
| 
 | |
|     const {newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic);
 | |
|     const shaDir = path.join(prDir, sha);
 | |
|     let dirToRemoveOnError: string;
 | |
| 
 | |
|     return Promise.resolve().
 | |
|       // If the same PR exists with different visibility, update the visibility first.
 | |
|       then(() => this.updatePrVisibility(pr, isPublic)).
 | |
|       then(() => Promise.all([this.exists(prDir), this.exists(shaDir)])).
 | |
|       then(([prDirExisted, shaDirExisted]) => {
 | |
|         if (shaDirExisted) {
 | |
|           const publicOrNot = isPublic ? 'public' : 'non-public';
 | |
|           throw new PreviewServerError(409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
 | |
|         }
 | |
| 
 | |
|         dirToRemoveOnError = prDirExisted ? shaDir : prDir;
 | |
| 
 | |
|         return Promise.resolve().
 | |
|           then(() => shell.mkdir('-p', shaDir)).
 | |
|           then(() => this.extractArchive(archivePath, shaDir)).
 | |
|           then(() => this.emit(CreatedBuildEvent.type, new CreatedBuildEvent(+pr, sha, isPublic))).
 | |
|           then(() => undefined);
 | |
|       }).
 | |
|       catch(err => {
 | |
|         if (dirToRemoveOnError) {
 | |
|           shell.rm('-rf', dirToRemoveOnError);
 | |
|         }
 | |
| 
 | |
|         if (!(err instanceof PreviewServerError)) {
 | |
|           err = new PreviewServerError(500, `Error while creating preview at: ${shaDir}\n${err}`);
 | |
|         }
 | |
| 
 | |
|         throw err;
 | |
|       });
 | |
|   }
 | |
| 
 | |
|   public updatePrVisibility(pr: number, makePublic: boolean): Promise<boolean> {
 | |
|     const {oldPrDir: otherVisPrDir, newPrDir: targetVisPrDir} = this.getCandidatePrDirs(pr, makePublic);
 | |
| 
 | |
|     return Promise.
 | |
|       all([this.exists(otherVisPrDir), this.exists(targetVisPrDir)]).
 | |
|       then(([otherVisPrDirExisted, targetVisPrDirExisted]) => {
 | |
|         if (!otherVisPrDirExisted) {
 | |
|           // No visibility change: Either the visibility is up-to-date or the PR does not exist.
 | |
|           return false;
 | |
|         } else if (targetVisPrDirExisted) {
 | |
|           // Error: Directories for both visibilities exist.
 | |
|           throw new PreviewServerError(409,
 | |
|               `Request to move '${otherVisPrDir}' to existing directory '${targetVisPrDir}'.`);
 | |
|         }
 | |
| 
 | |
|         // Visibility change: Moving `otherVisPrDir` to `targetVisPrDir`.
 | |
|         return Promise.resolve().
 | |
|           then(() => shell.mv(otherVisPrDir, targetVisPrDir)).
 | |
|           then(() => this.listShasByDate(targetVisPrDir)).
 | |
|           then(shas => this.emit(ChangedPrVisibilityEvent.type, new ChangedPrVisibilityEvent(+pr, shas, makePublic))).
 | |
|           then(() => true);
 | |
|       }).
 | |
|       catch(err => {
 | |
|         if (!(err instanceof PreviewServerError)) {
 | |
|           err = new PreviewServerError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`);
 | |
|         }
 | |
| 
 | |
|         throw err;
 | |
|       });
 | |
|   }
 | |
| 
 | |
|   // Methods - Protected
 | |
|   protected exists(fileOrDir: string): Promise<boolean> {
 | |
|     return new Promise(resolve => fs.access(fileOrDir, err => resolve(!err)));
 | |
|   }
 | |
| 
 | |
|   protected extractArchive(inputFile: string, outputDir: string): Promise<void> {
 | |
|     return new Promise<void>((resolve, reject) => {
 | |
|       const cmd = `tar --extract --gzip --directory "${outputDir}" --file "${inputFile}"`;
 | |
| 
 | |
|       cp.exec(cmd, (err, _stdout, stderr) => {
 | |
|         if (err) {
 | |
|           return reject(err);
 | |
|         }
 | |
| 
 | |
|         if (stderr) {
 | |
|           this.logger.warn(stderr);
 | |
|         }
 | |
| 
 | |
|         try {
 | |
|           shell.chmod('-R', 'a-w', outputDir);
 | |
|           shell.rm('-f', inputFile);
 | |
|           resolve();
 | |
|         } catch (err) {
 | |
|           reject(err);
 | |
|         }
 | |
|       });
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   protected getCandidatePrDirs(pr: number, isPublic: boolean): {oldPrDir: string, newPrDir: string} {
 | |
|     const hiddenPrDir = path.join(this.buildsDir, HIDDEN_DIR_PREFIX + pr);
 | |
|     const publicPrDir = path.join(this.buildsDir, `${pr}`);
 | |
| 
 | |
|     const oldPrDir = isPublic ? hiddenPrDir : publicPrDir;
 | |
|     const newPrDir = isPublic ? publicPrDir : hiddenPrDir;
 | |
| 
 | |
|     return {oldPrDir, newPrDir};
 | |
|   }
 | |
| 
 | |
|   protected listShasByDate(inputDir: string): Promise<string[]> {
 | |
|     return Promise.resolve().
 | |
|       then(() => shell.ls('-l', inputDir) as any as Promise<(fs.Stats & {name: string})[]>).
 | |
|       // Keep directories only.
 | |
|       // (Also, convert to standard Array - ShellJS provides custom `sort()` method for sorting file contents.)
 | |
|       then(items => items.filter(item => item.isDirectory())).
 | |
|       // Sort by modification date.
 | |
|       then(items => items.sort((a, b) => a.mtime.getTime() - b.mtime.getTime())).
 | |
|       // Return directory names.
 | |
|       then(items => items.map(item => item.name));
 | |
|   }
 | |
| }
 |