| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | // 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'; | 
					
						
							| 
									
										
										
										
											2017-06-25 22:13:03 +03:00
										 |  |  | import {HIDDEN_DIR_PREFIX, SHORT_SHA_LEN} from '../common/constants'; | 
					
						
							| 
									
										
										
										
											2017-02-27 21:04:43 +02:00
										 |  |  | import {assertNotMissingOrEmpty} from '../common/utils'; | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  | import {ChangedPrVisibilityEvent, CreatedBuildEvent} from './build-events'; | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | import {UploadError} from './upload-error'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Classes
 | 
					
						
							|  |  |  | export class BuildCreator extends EventEmitter { | 
					
						
							|  |  |  |   // Constructor
 | 
					
						
							|  |  |  |   constructor(protected buildsDir: string) { | 
					
						
							|  |  |  |     super(); | 
					
						
							| 
									
										
										
										
											2017-02-27 21:04:43 +02:00
										 |  |  |     assertNotMissingOrEmpty('buildsDir', buildsDir); | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Methods - Public
 | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |   public create(pr: string, sha: string, archivePath: string, isPublic: boolean): Promise<void> { | 
					
						
							| 
									
										
										
										
											2017-06-25 22:13:03 +03:00
										 |  |  |     // Use only part of the SHA for more readable URLs.
 | 
					
						
							|  |  |  |     sha = sha.substr(0, SHORT_SHA_LEN); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-26 13:31:15 +03:00
										 |  |  |     const {newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic); | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  |     const shaDir = path.join(prDir, sha); | 
					
						
							|  |  |  |     let dirToRemoveOnError: string; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |     return Promise.resolve(). | 
					
						
							|  |  |  |       // If the same PR exists with different visibility, update the visibility first.
 | 
					
						
							| 
									
										
										
										
											2017-06-26 13:31:15 +03:00
										 |  |  |       then(() => this.updatePrVisibility(pr, isPublic)). | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |       then(() => Promise.all([this.exists(prDir), this.exists(shaDir)])). | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  |       then(([prDirExisted, shaDirExisted]) => { | 
					
						
							|  |  |  |         if (shaDirExisted) { | 
					
						
							| 
									
										
										
										
											2017-08-03 11:40:39 +03:00
										 |  |  |           const publicOrNot = isPublic ? 'public' : 'non-public'; | 
					
						
							|  |  |  |           throw new UploadError(409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`); | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         dirToRemoveOnError = prDirExisted ? shaDir : prDir; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return Promise.resolve(). | 
					
						
							|  |  |  |           then(() => shell.mkdir('-p', shaDir)). | 
					
						
							|  |  |  |           then(() => this.extractArchive(archivePath, shaDir)). | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |           then(() => this.emit(CreatedBuildEvent.type, new CreatedBuildEvent(+pr, sha, isPublic))). | 
					
						
							|  |  |  |           then(() => undefined); | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  |       }). | 
					
						
							|  |  |  |       catch(err => { | 
					
						
							|  |  |  |         if (dirToRemoveOnError) { | 
					
						
							|  |  |  |           shell.rm('-rf', dirToRemoveOnError); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!(err instanceof UploadError)) { | 
					
						
							|  |  |  |           err = new UploadError(500, `Error while uploading to directory: ${shaDir}\n${err}`); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         throw err; | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-26 13:31:15 +03:00
										 |  |  |   public updatePrVisibility(pr: string, 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 UploadError(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 UploadError)) { | 
					
						
							|  |  |  |           err = new UploadError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         throw err; | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  |   // 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) { | 
					
						
							|  |  |  |           console.warn(stderr); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         try { | 
					
						
							| 
									
										
										
										
											2018-06-08 12:55:15 +03:00
										 |  |  |           shell.chmod('-R', 'a-w', outputDir); | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  |           shell.rm('-f', inputFile); | 
					
						
							|  |  |  |           resolve(); | 
					
						
							|  |  |  |         } catch (err) { | 
					
						
							|  |  |  |           reject(err); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  |   protected getCandidatePrDirs(pr: string, isPublic: boolean) { | 
					
						
							| 
									
										
										
										
											2017-06-25 01:40:04 +03:00
										 |  |  |     const hiddenPrDir = path.join(this.buildsDir, HIDDEN_DIR_PREFIX + pr); | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |     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)); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2017-02-06 20:40:28 +02:00
										 |  |  | } |