Skip to content

Commit ac53f08

Browse files
Merge pull request #200 from obukhovaa/feat/download-attachement
feat(attachement):download attachement, e.g. images
2 parents c1bce3d + e67e35d commit ac53f08

File tree

5 files changed

+80
-1
lines changed

5 files changed

+80
-1
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,8 @@ build
55
.env.local
66
.env.test
77
coverage/
8-
*.log
8+
*.log
9+
10+
# ai
11+
.opencode*
12+
.aider*

OpenCode.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# OpenCode Configuration
2+
3+
## Build/Test Commands
4+
- `npm run build` - Compile TypeScript to build/
5+
- `npm run dev` - Build and run the server
6+
- `npm run watch` - Watch mode compilation
7+
- `npm test` - Run API validation tests
8+
- `npm run test:integration` - Run integration tests
9+
- `npm run test:server` - Build and test all transport servers
10+
- `npm run lint` - ESLint check
11+
- `npm run lint:fix` - ESLint auto-fix
12+
- `npm run format` - Prettier format all files
13+
- `npm run format:check` - Check formatting
14+
15+
## Code Style Guidelines
16+
- **Language**: TypeScript with strict mode enabled
17+
- **Module System**: ES modules (type: "module" in package.json)
18+
- **Imports**: Use .js extensions for local imports, named imports preferred
19+
- **Types**: Explicit typing with Zod schemas, avoid `any` types
20+
- **Naming**: camelCase for variables/functions, PascalCase for types/schemas
21+
- **Error Handling**: Use structured error responses with proper HTTP status codes
22+
- **Logging**: Use pino logger with structured logging
23+
- **Schemas**: Define Zod schemas for all API inputs/outputs, export both type and schema
24+
- **File Structure**: Separate schemas in schemas.ts, custom utilities in customSchemas.ts

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ $ sh scripts/image_push.sh docker_user_name
244244
`delete_issue` - Delete an issue from a GitLab project
245245
`delete_issue_link` - Delete an issue link
246246
`delete_draft_note` - Delete a draft note
247+
`download_attachment` - Download an uploaded file from a GitLab project by secret and filename
247248
`create_wiki_page` - Create a new wiki page in a GitLab project
248249
`create_repository` - Create a new GitLab project
249250
`create_pipeline` - Create a new pipeline for a branch or tag

index.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ import {
167167
ListWikiPagesOptions,
168168
ListWikiPagesSchema,
169169
MarkdownUploadSchema,
170+
DownloadAttachmentSchema,
170171
MergeMergeRequestSchema,
171172
type MergeRequestThreadPosition,
172173
type MergeRequestThreadPositionCreate,
@@ -810,6 +811,11 @@ const allTools = [
810811
description: "Upload a file to a GitLab project for use in markdown content",
811812
inputSchema: zodToJsonSchema(MarkdownUploadSchema),
812813
},
814+
{
815+
name: "download_attachment",
816+
description: "Download an uploaded file from a GitLab project by secret and filename",
817+
inputSchema: zodToJsonSchema(DownloadAttachmentSchema),
818+
},
813819
];
814820

815821
// Define which tools are read-only
@@ -856,6 +862,7 @@ const readOnlyTools = [
856862
"get_commit_diff",
857863
"list_group_iterations",
858864
"get_group_iteration",
865+
"download_attachment",
859866
];
860867

861868
// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
@@ -4054,6 +4061,34 @@ async function markdownUpload(projectId: string, filePath: string): Promise<GitL
40544061
return GitLabMarkdownUploadSchema.parse(data);
40554062
}
40564063

4064+
async function downloadAttachment(projectId: string, secret: string, filename: string, localPath?: string): Promise<string> {
4065+
const effectiveProjectId = getEffectiveProjectId(projectId);
4066+
4067+
const url = new URL(
4068+
`${GITLAB_API_URL}/projects/${encodeURIComponent(effectiveProjectId)}/uploads/${secret}/${filename}`
4069+
);
4070+
4071+
const response = await fetch(url.toString(), {
4072+
method: "GET",
4073+
headers: DEFAULT_HEADERS,
4074+
});
4075+
4076+
if (!response.ok) {
4077+
await handleGitLabError(response);
4078+
}
4079+
4080+
// Get the file content as buffer
4081+
const buffer = await response.arrayBuffer();
4082+
4083+
// Determine the save path
4084+
const savePath = localPath ? path.join(localPath, filename) : filename;
4085+
4086+
// Write the file to disk
4087+
fs.writeFileSync(savePath, Buffer.from(buffer));
4088+
4089+
return savePath;
4090+
}
4091+
40574092
server.setRequestHandler(ListToolsRequestSchema, async () => {
40584093
// Apply read-only filter first
40594094
const tools0 = GITLAB_READ_ONLY_MODE
@@ -5133,6 +5168,14 @@ server.setRequestHandler(CallToolRequestSchema, async request => {
51335168
};
51345169
}
51355170

5171+
case "download_attachment": {
5172+
const args = DownloadAttachmentSchema.parse(request.params.arguments);
5173+
const filePath = await downloadAttachment(args.project_id, args.secret, args.filename, args.local_path);
5174+
return {
5175+
content: [{ type: "text", text: JSON.stringify({ success: true, file_path: filePath }, null, 2) }],
5176+
};
5177+
}
5178+
51365179
default:
51375180
throw new Error(`Unknown tool: ${request.params.name}`);
51385181
}

schemas.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1842,6 +1842,13 @@ export const MarkdownUploadSchema = z.object({
18421842
file_path: z.string().describe("Path to the file to upload"),
18431843
});
18441844

1845+
export const DownloadAttachmentSchema = z.object({
1846+
project_id: z.string().describe("Project ID or URL-encoded path of the project"),
1847+
secret: z.string().describe("The 32-character secret of the upload"),
1848+
filename: z.string().describe("The filename of the upload"),
1849+
local_path: z.string().optional().describe("Local path to save the file (optional, defaults to current directory)"),
1850+
});
1851+
18451852
export const GroupIteration = z.object({
18461853
id: z.coerce.string(),
18471854
iid: z.coerce.string(),

0 commit comments

Comments
 (0)