// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. 'use strict'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { getLogger, Logger } from '../../common/log'; import { uniqueString } from '../../common/utils'; import { tarAdd } from '../common/util'; export abstract class StorageService { protected localRoot: string = ""; protected remoteRoot: string = ""; protected logger: Logger; protected abstract internalConfig(key: string, value: string): void; protected abstract async internalRemove(remotePath: string, isDirectory: boolean, isRecursive: boolean): Promise; protected abstract async internalRename(remotePath: string, newName: string): Promise; protected abstract async internalMkdir(remotePath: string): Promise; protected abstract async internalCopy(sourcePath: string, targetPath: string, isDirectory: boolean, isFromRemote: boolean, isToRemote: boolean): Promise; protected abstract async internalExists(remotePath: string): Promise; protected abstract async internalRead(remotePath: string, offset: number, length: number): Promise; protected abstract async internalList(remotePath: string): Promise; protected abstract async internalAttach(remotePath: string, content: string): Promise; protected abstract internalIsRelativePath(path: string): boolean; protected abstract internalJoin(...paths: string[]): string; protected abstract internalDirname(...paths: string[]): string; protected abstract internalBasename(...paths: string[]): string; constructor() { this.logger = getLogger(); } public initialize(localRoot: string, remoteRoot: string): void { this.logger.debug(`Initializing storage to local: ${localRoot} remote: ${remoteRoot}`); this.localRoot = localRoot; this.remoteRoot = remoteRoot; } public async rename(remotePath: string, newName: string): Promise { remotePath = this.expandPath(true, remotePath); this.logger.debug(`rename remotePath: ${remotePath} to: ${newName}`); await this.internalRename(remotePath, newName); } public async createDirectory(remotePath: string): Promise { remotePath = this.expandPath(true, remotePath); this.logger.debug(`create remotePath: ${remotePath}`); await this.internalMkdir(remotePath); } public async copyDirectory(localPath: string, remotePath: string, asGzip: boolean = false): Promise { localPath = this.expandPath(false, localPath); remotePath = this.expandPath(true, remotePath); this.logger.debug(`copy localPath: ${localPath} to remotePath: ${remotePath}, asGzip ${asGzip}`); if (!await this.internalExists(remotePath)) { await this.internalMkdir(remotePath); } if (asGzip) { const localPathBaseName = path.basename(localPath); const tempTarFileName = `nni_tmp_${localPathBaseName}_${uniqueString(5)}.tar.gz`; const tarFileName = `${localPathBaseName}.tar.gz`; const localTarPath: string = path.join(os.tmpdir(), tempTarFileName); await tarAdd(localTarPath, localPath); await this.internalCopy(localTarPath, remotePath, false, false, true); const remoteFileName = this.internalJoin(remotePath, tempTarFileName); await this.internalRename(remoteFileName, tarFileName); await fs.promises.unlink(localTarPath); remotePath = this.internalJoin(remotePath, tarFileName); } else { await this.internalCopy(localPath, remotePath, true, false, true); remotePath = this.internalJoin(remotePath, path.basename(localPath)); } return remotePath; } public async copyDirectoryBack(remotePath: string, localPath: string): Promise { localPath = this.expandPath(false, localPath); remotePath = this.expandPath(true, remotePath); this.logger.debug(`copy remotePath: ${remotePath} to localPath: ${localPath}`); return await this.internalCopy(remotePath, localPath, true, true, false); } public async removeDirectory(remotePath: string, isRecursive: boolean): Promise { remotePath = this.expandPath(true, remotePath); this.logger.debug(`remove remotePath: ${remotePath}`); await this.internalRemove(remotePath, true, isRecursive); } public async readFileContent(remotePath: string, offset: number = -1, length: number = -1): Promise { remotePath = this.expandPath(true, remotePath); this.logger.debug(`read remote file: ${remotePath}, offset: ${offset}, length: ${length}`); return this.internalRead(remotePath, offset, length); } public async listDirectory(remotePath: string): Promise { remotePath = this.expandPath(true, remotePath); this.logger.debug(`list remotePath: ${remotePath}`); return await this.internalList(remotePath); } public async exists(remotePath: string): Promise { remotePath = this.expandPath(true, remotePath); const exists = await this.internalExists(remotePath); this.logger.debug(`exists remotePath: ${remotePath} is ${exists}`); return exists } public async save(content: string, remotePath: string, isAttach: boolean = false): Promise { remotePath = this.expandPath(true, remotePath); this.logger.debug(`saving content to remotePath: ${remotePath}, length: ${content.length}, isAttach: ${isAttach}`); const remoteDir = this.internalDirname(remotePath); if (isAttach) { if (await this.internalExists(remoteDir) === false) { await this.internalMkdir(remoteDir); } const result = await this.internalAttach(remotePath, content); if (false === result) { throw new Error("this.internalAttach doesn't support"); } } else { const fileName = this.internalBasename(remotePath); const tempFileName = `temp_${uniqueString(4)}_${fileName}`; const localTempFileName = path.join(os.tmpdir(), tempFileName); const remoteTempFile = this.internalJoin(remoteDir, tempFileName); if (await this.internalExists(remotePath) === true) { await this.internalRemove(remotePath, false, false); } await fs.promises.writeFile(localTempFileName, content); await this.internalCopy(localTempFileName, remoteDir, false, false, true); await this.rename(remoteTempFile, fileName); await fs.promises.unlink(localTempFileName); } } public async copyFile(localPath: string, remotePath: string): Promise { localPath = this.expandPath(false, localPath); remotePath = this.expandPath(true, remotePath); this.logger.debug(`copying file localPath: ${localPath} to remotePath: ${remotePath}`); await this.internalCopy(localPath, remotePath, false, false, true); } public async copyFileBack(remotePath: string, localPath: string): Promise { localPath = this.expandPath(false, localPath); remotePath = this.expandPath(true, remotePath); this.logger.debug(`copy file remotePath: ${remotePath} to localPath: ${localPath}`); await this.internalCopy(remotePath, localPath, false, true, false); } public async removeFile(remotePath: string): Promise { remotePath = this.expandPath(true, remotePath); this.logger.debug(`remove file remotePath: ${remotePath}`); await this.internalRemove(remotePath, false, false); } public joinPath(...paths: string[]): string { let fullPath = this.internalJoin(...paths); if (this.internalIsRelativePath(fullPath) === true && this.remoteRoot !== "") { fullPath = this.internalJoin(this.remoteRoot, fullPath); } return fullPath; } private expandPath(isRemote: boolean, ...paths: string[]): string { let normalizedPath: string; if (isRemote) { normalizedPath = this.joinPath(...paths); } else { normalizedPath = path.join(...paths); if (!path.isAbsolute(normalizedPath) && this.localRoot !== "") { normalizedPath = path.join(this.localRoot, normalizedPath); } } return normalizedPath; } }