Skip to content

Instantly share code, notes, and snippets.

@prashant1k99
Last active January 3, 2025 10:24
Show Gist options
  • Save prashant1k99/d1da73e4cd8911f5d27678196240c11e to your computer and use it in GitHub Desktop.
Save prashant1k99/d1da73e4cd8911f5d27678196240c11e to your computer and use it in GitHub Desktop.
This Deno server uploads file to github repo using github app and also can delete for Delete endpoint
import { createAppAuth } from "npm:@octokit/auth-app";
import { Octokit } from "npm:@octokit/rest";
import { serve } from "https://deno.land/std/http/server.ts";
import { ContentType } from "npm:@octokit/types";
// Configuration
const APP_ID = Deno.env.get("GITHUB_APP_ID") || "";
const PRIVATE_KEY = Deno.env.get("GITHUB_PRIVATE_KEY") || "";
const INSTALLATION_ID = Deno.env.get("GITHUB_INSTALLATION_ID") || "";
const OWNER = Deno.env.get("REPO_OWNER") || "";
const REPO = Deno.env.get("REPO_NAME") || "";
const ALLOWED_DOMAINS = Deno.env.get("ALLOWED_DOMAINS") || "*";
const ALLOWED_CONTENT_TYPES = new Set([
'text/plain',
'text/markdown',
'text/html',
'application/json',
'image/png',
'image/jpeg',
'image/gif',
'image/svg+xml',
'application/pdf',
'application/xml',
'text/css',
'application/javascript'
]);
const MAX_FILE_SIZE = 2 * 1024 * 1024;
interface UploadRequest {
content: string | Uint8Array;
path: string;
message?: string;
contentType: string;
}
function validateContentType(contentType: string): boolean {
return ALLOWED_CONTENT_TYPES.has(contentType.toLowerCase());
}
function validateDomain(origin: string | null): boolean {
// Allow all domains if ALLOWED_DOMAINS is "*"
if (ALLOWED_DOMAINS === "*") return true;
if (!origin) return false;
// Split domains and check each
const allowedDomainsList = ALLOWED_DOMAINS.split(",").map(d => d.trim());
return allowedDomainsList.some(domain =>
origin === domain || origin.endsWith(`.${domain}`)
);
}
function getCorsHeaders(origin: string | null): HeadersInit {
// If allowing all domains, return "*" or the specific origin
if (ALLOWED_DOMAINS === "*") {
return {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization"
};
}
// Otherwise, return validated origin
return {
"Access-Control-Allow-Origin": validateDomain(origin) ? origin! : "",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Vary": "Origin"
};
}
async function processContent(content: string | Uint8Array, contentType: string): Promise<string> {
// If content is already a string and not binary, return base64
if (typeof content === 'string') {
const encoder = new TextEncoder();
return btoa(String.fromCharCode(...encoder.encode(content)));
}
// Handle binary data
if (content instanceof Uint8Array) {
// Process the Uint8Array in chunks to avoid exceeding the call stack limit
let binaryString = "";
const chunkSize = 8192; // Process 8 KB at a time
for (let i = 0; i < content.length; i += chunkSize) {
binaryString += String.fromCharCode(...content.slice(i, i + chunkSize));
}
return btoa(binaryString);
}
throw new Error("Unsupported content format");
}
async function initializeOctokit() {
const auth = createAppAuth({
appId: APP_ID,
privateKey: PRIVATE_KEY,
installationId: INSTALLATION_ID,
});
const installationAuthentication = await auth({ type: "installation" });
return new Octokit({
auth: installationAuthentication.token,
});
}
async function uploadToGithub({ content, path, message = "File upload via API", contentType }: UploadRequest) {
try {
const octokit = await initializeOctokit();
// Check if file exists
let sha: string | undefined;
try {
const { data: existingFile } = await octokit.rest.repos.getContent({
owner: OWNER,
repo: REPO,
path,
});
if (!Array.isArray(existingFile)) {
sha = existingFile.sha;
}
} catch (error) {
console.log("File doesn't exist yet, creating new file");
}
// Process content based on content type
const contentEncoded = await processContent(content, contentType);
// Create or update file
const response = await octokit.rest.repos.createOrUpdateFileContents({
owner: OWNER,
repo: REPO,
path,
message,
content: contentEncoded,
...(sha && { sha }),
});
return {
success: true,
data: response.data,
};
} catch (error) {
console.error("Upload error:", error);
return {
success: false,
error: error.message,
details: error.response?.data || "No additional details available"
};
}
}
async function deleteFile(path: string, message = "File deleted via API") {
try {
const octokit = await initializeOctokit();
// Get the file's SHA
const { data: existingFile } = await octokit.rest.repos.getContent({
owner: OWNER,
repo: REPO,
path,
});
if (Array.isArray(existingFile)) {
throw new Error("Path points to a directory, not a file");
}
// Delete the file
const response = await octokit.rest.repos.deleteFile({
owner: OWNER,
repo: REPO,
path,
message,
sha: existingFile.sha,
});
return {
success: true,
data: response.data
};
} catch (error) {
console.error("Delete error:", error);
return {
success: false,
error: error.message,
details: error.response?.data || "No additional details available"
};
}
}
serve(async (req: Request) => {
const origin = req.headers.get("origin");
// CORS preflight
if (req.method === "OPTIONS") {
return new Response(null, {
headers: {
...getCorsHeaders(origin),
"Access-Control-Allow-Methods": "POST, DELETE, OPTIONS"
}
});
}
// Validate domain (skip validation if ALLOWED_DOMAINS is "*")
if (ALLOWED_DOMAINS !== "*" && !validateDomain(origin)) {
return new Response(JSON.stringify({
error: "Unauthorized domain"
}), {
status: 403,
headers: {
"Content-Type": "application/json"
}
});
}
// Handle DELETE request
if (req.method === "DELETE") {
const url = new URL(req.url);
const path = url.searchParams.get("path");
const message = url.searchParams.get("message") || "File deleted via API";
if (!path) {
return new Response(JSON.stringify({ error: "Path parameter is required" }), {
status: 400,
headers: {
"Content-Type": "application/json",
...getCorsHeaders(origin)
}
});
}
const result = await deleteFile(path, message);
return new Response(JSON.stringify(result), {
status: result.success ? 200 : 500,
headers: {
"Content-Type": "application/json",
...getCorsHeaders(origin)
}
});
}
if (req.method !== "POST") {
return new Response(JSON.stringify({ error: "Method not allowed" }), {
status: 405,
headers: {
"Content-Type": "application/json",
...getCorsHeaders(origin)
},
});
}
try {
const formData = await req.formData();
const file = formData.get("file") as File;
const path = formData.get("path") as string;
const message = formData.get("message") as string;
if (!file || !path) {
return new Response(
JSON.stringify({ error: "File and path are required" }), {
status: 400,
headers: {
"Content-Type": "application/json",
...getCorsHeaders(origin)
},
}
);
}
// Validate file size
if (file.size > MAX_FILE_SIZE) {
return new Response(
JSON.stringify({ error: "File exceeds the maximum allowed size of 2 MB" }),
{
status: 413, // HTTP 413 Payload Too Large
headers: {
"Content-Type": "application/json",
...getCorsHeaders(origin),
},
}
);
}
// Read file content
const content = await file.arrayBuffer();
const contentType = file.type || 'application/octet-stream';
const result = await uploadToGithub({
content: new Uint8Array(content),
path,
message,
contentType
});
return new Response(JSON.stringify(result), {
status: result.success ? 200 : 500,
headers: {
"Content-Type": "application/json",
...getCorsHeaders(origin)
},
});
} catch (error) {
return new Response(
JSON.stringify({
error: "Request processing failed",
details: error.message
}), {
status: 400,
headers: {
"Content-Type": "application/json",
...getCorsHeaders(origin)
},
}
);
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment