UNPKG

@mieweb/wikigdrive

Version:

Google Drive to MarkDown synchronization

280 lines (279 loc) 10.7 kB
import path from 'node:path'; import { PassThrough } from 'node:stream'; // import {Buffer} from 'https://deno.land/std/io/buffer.ts'; import Docker from 'dockerode'; import tarFs from 'tar-fs'; import tarStream from 'tar-stream'; import { BufferWritable } from '../../utils/BufferWritable.js'; import process from 'node:process'; import fs from 'node:fs'; export class DockerContainer { constructor(logger, id, image, container, volume, dirs) { Object.defineProperty(this, "logger", { enumerable: true, configurable: true, writable: true, value: logger }); Object.defineProperty(this, "id", { enumerable: true, configurable: true, writable: true, value: id }); Object.defineProperty(this, "image", { enumerable: true, configurable: true, writable: true, value: image }); Object.defineProperty(this, "container", { enumerable: true, configurable: true, writable: true, value: container }); Object.defineProperty(this, "volume", { enumerable: true, configurable: true, writable: true, value: volume }); Object.defineProperty(this, "dirs", { enumerable: true, configurable: true, writable: true, value: dirs }); } static async create(logger, image, env, repoSubDir) { // https://github.com/apocas/dockerode/issues/747 // const dockerEngine = new Docker({socketPath: '/var/run/docker.sock'}); const dockerEngine = new Docker({ protocol: 'http', host: '127.0.0.1', port: 5000, timeout: 30 * 1000 }); const upper = fs.mkdtempSync(path.join('/srv/overlay_mounts', `${env.DRIVE_ID}-upper`)); const workdir = fs.mkdtempSync(path.join('/srv/overlay_mounts', `${env.DRIVE_ID}-workdir`)); try { const container = await dockerEngine.getContainer(`${env.DRIVE_ID}_job`); if (container) { await container.remove({ force: true }); } // deno-lint-ignore no-unused-vars } catch (ignoredError) { /* empty */ } try { const volume = await dockerEngine.getVolume(`${env.DRIVE_ID}_overlay_site`); if (volume) { await volume.remove({ force: true }); } // deno-lint-ignore no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (ignoredError) { /* empty */ } const volume = await dockerEngine.createVolume({ 'Name': `${env.DRIVE_ID}_overlay_site`, 'Driver': 'local', 'DriverOpts': { 'type': 'overlay', 'o': `lowerdir=${process.env.VOLUME_DATA}/${repoSubDir},upperdir=${upper},workdir=${workdir}`, 'device': 'overlay' } }); const container = await dockerEngine.createContainer({ Name: `${env.DRIVE_ID}_job`, Image: image, AttachStdin: false, AttachStdout: true, AttachStderr: true, Tty: true, OpenStdin: false, StdinOnce: false, HostConfig: { AutoRemove: true, Binds: [ `${process.env.VOLUME_DATA}/${env.DRIVE_ID}/action-cache:/action-cache:rw`, `${process.env.VOLUME_PREVIEW}/${env.DRIVE_ID}:/output-preview:rw` // `${process.env.VOLUME_DATA}/${driveId}_transform:/repo:ro`, // `${process.env.VOLUME_DATA}/${driveIdTransform}:/site:rw`, // `${process.env.VOLUME_DATA}${contentDir}:/site/content:rw`, ], Mounts: [ { 'Type': 'volume', 'Source': `${env.DRIVE_ID}_overlay_site`, 'Target': '/site', 'VolumeOptions': { 'Driver': 'local', 'DriverOpts': { 'type': 'overlay', 'o': `lowerdir=${process.env.VOLUME_DATA}${repoSubDir},upperdir=${upper},workdir=${workdir}`, 'device': 'overlay' } } } ] }, Env: Object.keys(env).map(key => `${key}=${env[key]}`), User: String(process.getuid()) + ':' + String(process.getegid()) }); //--user=$(id -u):$(getent group docker | cut -d: -f3) // logger.info(`DockerAPI:\ndocker start \\ // --user=${process.getuid()}:${process.getegid()} \\ // // -v "${process.env.VOLUME_DATA}/${driveId}_transform:/repo:ro" \\ // // -v "${process.env.VOLUME_DATA}/${driveIdTransform}:/site:rw" \\ // // --mount "type=tmpfs,destination=/site/resources" \\ // ${Object.keys(env).map(key => `--env ${key}="${env[key]}"`).join(' ')} \\ // ${process.env.ACTION_IMAGE} // `); return new DockerContainer(logger, container.id, image, container, volume, [upper, workdir]); } async start() { await this.container.start(); this.logger.info('docker started: ' + this.id); } async stop() { try { return await this.container.stop({ t: 0 }); } catch (err) { this.logger.error(err.stack ? err.stack : err.message); } } async remove() { setTimeout(() => { for (const dir of this.dirs) { fs.rmSync(dir, { recursive: true, force: true }); } }, 30 * 1000); } async copy(realPath, remotePath, ignoreGit = false) { this.logger.info('docker cp into ' + remotePath); const archive = tarFs.pack(realPath, { ignore(name) { if (name.startsWith(path.join(realPath, '.private'))) { return true; } if (ignoreGit && name.startsWith(path.join(realPath, '.git', 'lfs'))) { return true; } if (ignoreGit && name.startsWith(path.join(realPath, '.git'))) { return true; } if (name.endsWith('.debug.xml')) { return true; } return false; }, }); await this.container.putArchive(archive, { path: remotePath }); } async putFile(content, remotePath) { const archive = tarStream.pack(); archive.entry({ name: remotePath }, content); archive.finalize(); const writable = new BufferWritable(); archive.pipe(writable); this.logger.info('docker write into ' + remotePath); await this.container.putArchive(writable.getBuffer(), { path: '/' }); } async export(remotePath, outputDir) { this.logger.info('docker export ' + remotePath); const archive = await this.container.getArchive({ path: remotePath }); await new Promise((resolve, reject) => { try { const stream = archive.pipe(tarFs.extract(outputDir, { map(header) { const parts = header.name.split('/'); parts.shift(); header.name = parts.join('/'); return header; } })); stream.on('finish', () => { resolve(); }); stream.on('error', (err) => { reject(err); }); } catch (err) { reject(err); } }); } async getFile(remotePath) { const archive = await this.container.getArchive({ path: remotePath }); return await new Promise((resolve, reject) => { const retVal = []; const extract = tarStream.extract(); extract.on('entry', (header, stream, next) => { stream.on('data', (data) => { retVal.push(new Uint8Array(data.buffer, data.byteOffset, data.length)); }); stream.on('end', () => { next(); }); stream.resume(); }); try { const stream = archive.pipe(extract); stream.on('finish', () => { const totalLength = retVal.reduce((acc, arr) => acc + arr.length, 0); const combinedArray = new Uint8Array(totalLength); let offset = 0; retVal.forEach(arr => { combinedArray.set(arr, offset); // Copy each array into the combined one offset += arr.length; // Update the offset for the next array }); resolve(combinedArray); }); stream.on('error', (err) => { reject(err); }); } catch (err) { reject(err); } }); } async exec(command, env, writable) { this.logger.info(`docker exec ${this.id} ${command}`); const cancelTimeout = new AbortController(); const exec = await this.container.exec({ Cmd: command.split(' '), AttachStdin: false, AttachStdout: true, AttachStderr: true, Tty: true, Env: Object.keys(env).map(key => `${key}=${env[key]}`), //WorkingDir abortSignal: cancelTimeout.signal, }); const stream = await exec.start({}); const stdout = new PassThrough(); const stderr = new PassThrough(); this.container.modem.demuxStream(stream, stdout, stderr); stdout.on('data', (chunk) => { writable.write(chunk); }); stderr.on('data', (chunk) => { writable.write(chunk); }); await new Promise(resolve => stream.on('end', () => { resolve(0); })); const inspectInfo = await exec.inspect(); return inspectInfo.ExitCode; } }