mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-18 01:51:20 -07:00
use extract-zip and tar libraries to extract archives (#10414)
Co-authored-by: christine betts <chrstn@uw.edu>
This commit is contained in:
@@ -8,12 +8,18 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
checkForExtensionUpdate,
|
||||
cloneFromGit,
|
||||
extractFile,
|
||||
findReleaseAsset,
|
||||
parseGitHubRepoForReleases,
|
||||
} from './github.js';
|
||||
import { simpleGit, type SimpleGit } from 'simple-git';
|
||||
import { ExtensionUpdateState } from '../../ui/state/extensions.js';
|
||||
import type * as os from 'node:os';
|
||||
import * as os from 'node:os';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as fsSync from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as tar from 'tar';
|
||||
import * as archiver from 'archiver';
|
||||
import type { GeminiCLIExtension } from '@google/gemini-cli-core';
|
||||
|
||||
const mockPlatform = vi.hoisted(() => vi.fn());
|
||||
@@ -328,4 +334,83 @@ describe('git extension helpers', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractFile', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gemini-test-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should extract a .tar.gz file', async () => {
|
||||
const archivePath = path.join(tempDir, 'test.tar.gz');
|
||||
const extractionDest = path.join(tempDir, 'extracted');
|
||||
await fs.mkdir(extractionDest);
|
||||
|
||||
// Create a dummy file to be archived
|
||||
const dummyFilePath = path.join(tempDir, 'test.txt');
|
||||
await fs.writeFile(dummyFilePath, 'hello tar');
|
||||
|
||||
// Create the tar.gz file
|
||||
await tar.c(
|
||||
{
|
||||
gzip: true,
|
||||
file: archivePath,
|
||||
cwd: tempDir,
|
||||
},
|
||||
['test.txt'],
|
||||
);
|
||||
|
||||
await extractFile(archivePath, extractionDest);
|
||||
|
||||
const extractedFilePath = path.join(extractionDest, 'test.txt');
|
||||
const content = await fs.readFile(extractedFilePath, 'utf-8');
|
||||
expect(content).toBe('hello tar');
|
||||
});
|
||||
|
||||
it('should extract a .zip file', async () => {
|
||||
const archivePath = path.join(tempDir, 'test.zip');
|
||||
const extractionDest = path.join(tempDir, 'extracted');
|
||||
await fs.mkdir(extractionDest);
|
||||
|
||||
// Create a dummy file to be archived
|
||||
const dummyFilePath = path.join(tempDir, 'test.txt');
|
||||
await fs.writeFile(dummyFilePath, 'hello zip');
|
||||
|
||||
// Create the zip file
|
||||
const output = fsSync.createWriteStream(archivePath);
|
||||
const archive = archiver.create('zip');
|
||||
|
||||
const streamFinished = new Promise((resolve, reject) => {
|
||||
output.on('close', () => resolve(null));
|
||||
archive.on('error', reject);
|
||||
});
|
||||
|
||||
archive.pipe(output);
|
||||
archive.file(dummyFilePath, { name: 'test.txt' });
|
||||
await archive.finalize();
|
||||
await streamFinished;
|
||||
|
||||
await extractFile(archivePath, extractionDest);
|
||||
|
||||
const extractedFilePath = path.join(extractionDest, 'test.txt');
|
||||
const content = await fs.readFile(extractedFilePath, 'utf-8');
|
||||
expect(content).toBe('hello zip');
|
||||
});
|
||||
|
||||
it('should throw an error for unsupported file types', async () => {
|
||||
const unsupportedFilePath = path.join(tempDir, 'test.txt');
|
||||
await fs.writeFile(unsupportedFilePath, 'some content');
|
||||
const extractionDest = path.join(tempDir, 'extracted');
|
||||
await fs.mkdir(extractionDest);
|
||||
|
||||
await expect(
|
||||
extractFile(unsupportedFilePath, extractionDest),
|
||||
).rejects.toThrow('Unsupported file extension for extraction:');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,8 +15,9 @@ import * as os from 'node:os';
|
||||
import * as https from 'node:https';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { EXTENSIONS_CONFIG_FILENAME, loadExtension } from '../extension.js';
|
||||
import * as tar from 'tar';
|
||||
import extract from 'extract-zip';
|
||||
|
||||
function getGitHubToken(): string | undefined {
|
||||
return process.env['GITHUB_TOKEN'];
|
||||
@@ -258,7 +259,7 @@ export async function downloadFromGitHubRelease(
|
||||
|
||||
await downloadFile(archiveUrl, downloadedAssetPath);
|
||||
|
||||
extractFile(downloadedAssetPath, destination);
|
||||
await extractFile(downloadedAssetPath, destination);
|
||||
|
||||
// For regular github releases, the repository is put inside of a top level
|
||||
// directory. In this case we should see exactly two file in the destination
|
||||
@@ -404,27 +405,15 @@ async function downloadFile(url: string, dest: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
function extractFile(file: string, dest: string) {
|
||||
let args: string[];
|
||||
let command: 'tar' | 'unzip';
|
||||
export async function extractFile(file: string, dest: string): Promise<void> {
|
||||
if (file.endsWith('.tar.gz')) {
|
||||
args = ['-xzf', file, '-C', dest];
|
||||
command = 'tar';
|
||||
await tar.x({
|
||||
file,
|
||||
cwd: dest,
|
||||
});
|
||||
} else if (file.endsWith('.zip')) {
|
||||
args = [file, '-d', dest];
|
||||
command = 'unzip';
|
||||
await extract(file, { dir: dest });
|
||||
} else {
|
||||
throw new Error(`Unsupported file extension for extraction: ${file}`);
|
||||
}
|
||||
|
||||
const result = spawnSync(command, args, { stdio: 'pipe' });
|
||||
|
||||
if (result.status !== 0) {
|
||||
if (result.error) {
|
||||
throw new Error(`Failed to spawn '${command}': ${result.error.message}`);
|
||||
}
|
||||
throw new Error(
|
||||
`'${command}' command failed with exit code ${result.status}: ${result.stderr.toString()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user