Skip to content

Commit a613958

Browse files
feat: add github extraction feature
1 parent d9cc6bc commit a613958

4 files changed

Lines changed: 166 additions & 8 deletions

File tree

dist/index.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/server/queue/jobQueue.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as path from 'path'
55
import * as fs from 'fs-extra'
66
import { tmpdir } from 'os'
77
import * as YAML from 'yaml'
8+
import { removeDirectory } from '../utils/zipExtractor'
89

910
export interface ExportJob {
1011
id: string
@@ -183,8 +184,13 @@ export class JobQueue extends EventEmitter {
183184
inputFile = readmeFile ? readmeFile.path : job.source.files[0].path
184185
}
185186
} else if (job.source.type === 'git' && job.source.gitUrl) {
186-
// For git repos, we'd need to clone first - not implemented yet
187-
throw new Error('Git repository export not yet implemented')
187+
// Use main file from cloned git repository
188+
if ((job.source as any).mainFile) {
189+
inputFile = (job.source as any).mainFile
190+
console.log(`Using main markdown from Git: ${inputFile}`)
191+
} else {
192+
throw new Error('No main file found in Git repository')
193+
}
188194
} else {
189195
throw new Error('No valid input source')
190196
}
@@ -445,13 +451,55 @@ export class JobQueue extends EventEmitter {
445451
}
446452

447453
console.log(`Export completed: ${job.id} -> ${outputPath}`)
454+
455+
// Cleanup temporary directories
456+
try {
457+
// Clean up git clone directory if it exists
458+
if (job.source.type === 'git' && (job.source as any).cloneDir) {
459+
console.log(
460+
`Cleaning up git clone: ${(job.source as any).cloneDir}`,
461+
)
462+
await removeDirectory((job.source as any).cloneDir)
463+
}
464+
465+
// Clean up upload directory if it exists
466+
if (
467+
job.source.type === 'upload' &&
468+
(job.source as any).uploadDir
469+
) {
470+
console.log(
471+
`Cleaning up upload directory: ${(job.source as any).uploadDir}`,
472+
)
473+
await removeDirectory((job.source as any).uploadDir)
474+
}
475+
} catch (cleanupError) {
476+
console.warn(`Cleanup warning for job ${job.id}:`, cleanupError)
477+
// Don't fail the job if cleanup fails
478+
}
479+
448480
resolve()
449481
} catch (error) {
450482
reject(error)
451483
}
452484
})
453485
} catch (error) {
454486
console.error(`Export failed for job ${job.id}:`, error)
487+
488+
// Cleanup on error as well
489+
try {
490+
if (job.source.type === 'git' && (job.source as any).cloneDir) {
491+
await removeDirectory((job.source as any).cloneDir)
492+
}
493+
if (job.source.type === 'upload' && (job.source as any).uploadDir) {
494+
await removeDirectory((job.source as any).uploadDir)
495+
}
496+
} catch (cleanupError) {
497+
console.warn(
498+
`Cleanup error after failed job ${job.id}:`,
499+
cleanupError,
500+
)
501+
}
502+
455503
reject(error)
456504
}
457505
})

src/server/routes/export.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { randomUUID } from 'crypto'
66
import { tmpdir } from 'os'
77
import { fileURLToPath } from 'url'
88
import * as YAML from 'yaml'
9-
import { extractZip, findMainMarkdown, isZipFile } from '../utils/zipExtractor'
9+
import {
10+
extractZip,
11+
findMainMarkdown,
12+
isZipFile,
13+
cloneGitRepo,
14+
} from '../utils/zipExtractor'
1015
import { dirname } from '../../export/helper'
1116

1217
export const exportRouter: FastifyPluginAsync = async (fastify) => {
@@ -54,7 +59,7 @@ export const exportRouter: FastifyPluginAsync = async (fastify) => {
5459
// Save file temporarily
5560
const filepath = join(uploadDir, part.filename)
5661
const buffer = await part.toBuffer()
57-
await writeFile(filepath, buffer)
62+
await writeFile(filepath, new Uint8Array(buffer))
5863

5964
files.push({
6065
filename: part.filename,
@@ -117,7 +122,50 @@ export const exportRouter: FastifyPluginAsync = async (fastify) => {
117122
jobData.source.extractedFrom = zipFile.filename
118123
}
119124
} else if (jobData.source.gitUrl) {
125+
// Handle git repository cloning
120126
jobData.source.type = 'git'
127+
128+
fastify.log.info(`Cloning git repository: ${jobData.source.gitUrl}`)
129+
130+
// Create clone directory
131+
const cloneId = randomUUID()
132+
const cloneDir = join(tmpdir(), 'liaex-git', cloneId)
133+
await mkdir(cloneDir, { recursive: true })
134+
135+
try {
136+
// Clone the repository
137+
const repoPath = await cloneGitRepo(
138+
jobData.source.gitUrl,
139+
cloneDir,
140+
jobData.source.gitBranch,
141+
jobData.source.gitSubdir,
142+
)
143+
144+
fastify.log.info(`Git repository cloned to: ${repoPath}`)
145+
146+
// Find main markdown file
147+
const mainMarkdown = await findMainMarkdown(repoPath)
148+
if (!mainMarkdown) {
149+
return reply.code(400).send({
150+
error:
151+
'No markdown file found in Git repository. Please include a README.md or any .md file.',
152+
})
153+
}
154+
155+
fastify.log.info(`Found main markdown: ${mainMarkdown}`)
156+
157+
// Update job data with cloned repo information
158+
jobData.source.mainFile = mainMarkdown
159+
jobData.source.cloneDir = cloneDir
160+
jobData.source.repoPath = repoPath
161+
} catch (error: any) {
162+
fastify.log.error(
163+
`Failed to clone git repository: ${error.message}`,
164+
)
165+
return reply.code(400).send({
166+
error: `Failed to clone git repository: ${error.message}`,
167+
})
168+
}
121169
} else {
122170
return reply.code(400).send({
123171
error: 'No files uploaded and no git URL provided',

src/server/utils/zipExtractor.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { createReadStream } from 'fs'
2-
import { readdir, stat } from 'fs/promises'
2+
import { readdir, stat, rm } from 'fs/promises'
33
import { join, extname } from 'path'
44
import unzipper from 'unzipper'
5+
import { exec } from 'child_process'
6+
import { promisify } from 'util'
7+
8+
const execAsync = promisify(exec)
59

610
/**
711
* Extracts a ZIP file to a target directory
@@ -91,3 +95,61 @@ export async function findMainMarkdown(dir: string): Promise<string | null> {
9195
export function isZipFile(filename: string): boolean {
9296
return extname(filename).toLowerCase() === '.zip'
9397
}
98+
99+
/**
100+
* Clones a Git repository to a target directory
101+
* @param gitUrl Git repository URL
102+
* @param cloneDir Directory where to clone the repository
103+
* @param branch Optional branch or tag to checkout (defaults to 'main')
104+
* @param subdir Optional subdirectory within the repo to use as root
105+
* @returns Path to the cloned directory (including subdir if specified)
106+
*/
107+
export async function cloneGitRepo(
108+
gitUrl: string,
109+
cloneDir: string,
110+
branch?: string,
111+
subdir?: string,
112+
): Promise<string> {
113+
try {
114+
// Build git clone command
115+
const branchArg = branch ? `--branch ${branch}` : ''
116+
const cmd = `git clone --depth 1 ${branchArg} "${gitUrl}" "${cloneDir}"`
117+
118+
console.log(`Cloning git repository: ${cmd}`)
119+
const { stdout, stderr } = await execAsync(cmd)
120+
121+
if (stderr && !stderr.includes('Cloning into')) {
122+
console.warn('Git clone warnings:', stderr)
123+
}
124+
if (stdout) {
125+
console.log('Git clone output:', stdout)
126+
}
127+
128+
// If subdirectory specified, return the full path to it
129+
const finalPath = subdir ? join(cloneDir, subdir) : cloneDir
130+
131+
// Verify the path exists
132+
try {
133+
await stat(finalPath)
134+
} catch (error) {
135+
throw new Error(`Subdirectory '${subdir}' not found in cloned repository`)
136+
}
137+
138+
return finalPath
139+
} catch (error: any) {
140+
console.error('Git clone failed:', error)
141+
throw new Error(`Failed to clone git repository: ${error.message}`)
142+
}
143+
}
144+
145+
/**
146+
* Removes a directory and all its contents
147+
* @param dir Directory to remove
148+
*/
149+
export async function removeDirectory(dir: string): Promise<void> {
150+
try {
151+
await rm(dir, { recursive: true, force: true })
152+
} catch (error) {
153+
console.warn(`Failed to remove directory ${dir}:`, error)
154+
}
155+
}

0 commit comments

Comments
 (0)