@mieweb/wikigdrive
Version:
Google Drive to MarkDown synchronization
280 lines (279 loc) • 10.7 kB
JavaScript
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;
}
}