Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 11 additions & 125 deletions src/app/api/upload/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
import JSZip from "jszip";
import { ManifestParser } from "@/lib/extract-tools/manifest";
import slugify from "slugify";
import { LocalFSAdapter } from "@/lib/adapters/LocalFSAdapter";
import { UPLOAD_DIR } from "@/constants";
import { env } from "@/env";
import { parsePlist } from "@/lib/extract-tools/plist-parse";
import { AdapterError } from "@/lib/adapters/Errors";

const ALLOWED_EXTENSIONS = ["apk", "ipa"];

Expand Down Expand Up @@ -41,124 +36,15 @@ export async function PUT(req: Request) {
);
}

const appSlug = `${slugify(appName, {
lower: true,
remove: /[*+~.()'"!:@\/]/g,
})}`;
try {
const adapter = new LocalFSAdapter({ uploadDir: UPLOAD_DIR });

const artifactFile = await artifact.arrayBuffer();
const artifactBuffer = Buffer.from(artifactFile);

const dirPath = path.join(UPLOAD_DIR, appSlug);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}

switch (extension) {
case "apk": {
const archive = await JSZip.loadAsync(artifactBuffer);

const manifestBuffer = await archive
.file("AndroidManifest.xml")
?.async("arraybuffer");
if (!manifestBuffer) {
return NextResponse.json(
{ message: "Invalid APK file, no AndroidManifest.xml found" },
{ status: 400 }
);
}

const manifest = new ManifestParser(Buffer.from(manifestBuffer)).parse();
const versionCode = manifest.versionCode as number | undefined;
const packageName = manifest.package as string | undefined;

if (!versionCode || !packageName) {
return NextResponse.json(
{ message: "Invalid APK file, no versionCode or package name found" },
{ status: 400 }
);
}

// cleanup old apk files
const files = fs.readdirSync(dirPath);
for (const file of files) {
if (file.endsWith(".apk") || file.endsWith(".android.json")) {
fs.unlinkSync(path.join(dirPath, file));
}
}

// write new apk file
fs.writeFileSync(path.join(dirPath, `android.apk`), artifactBuffer);
// write metadata
fs.writeFileSync(
path.join(dirPath, `metadata.android.json`),
JSON.stringify(manifest, null, 2)
);

return NextResponse.json(`${env.HOST}/build/${appSlug}`);
}

case "ipa": {
const archive = await JSZip.loadAsync(artifactBuffer);

const rawInfoPlist = await archive
.file(/Payload\/[^/]+\/Info.plist/)[0]
?.async("uint8array");

if (!rawInfoPlist) {
return NextResponse.json(
{ message: "Invalid IPA file, no Info.plist found" },
{ status: 400 }
);
}

const plist = parsePlist(rawInfoPlist) as Record<string, any> | undefined;

if (typeof plist !== "object") {
return NextResponse.json(
{ message: "Invalid IPA file, Info.plist is not a valid plist" },
{ status: 400 }
);
}

const version = plist.CFBundleVersion as string | undefined;
const bundleId = plist.CFBundleIdentifier as string | undefined;

if (!version || !bundleId) {
return NextResponse.json(
{
message:
"Invalid IPA file, no CFBundleVersion or CFBundleIdentifier found",
},
{ status: 400 }
);
}

// cleanup old ipa files
const files = fs.readdirSync(dirPath);
for (const file of files) {
if (file.endsWith(".ipa") || file.endsWith(".ios.json")) {
fs.unlinkSync(path.join(dirPath, file));
}
}

// write new ipa file
fs.writeFileSync(path.join(dirPath, `ios.ipa`), artifactBuffer);

// write metadata
fs.writeFileSync(
path.join(dirPath, `metadata.ios.json`),
JSON.stringify(plist, null, 2)
);

return NextResponse.json(`${env.HOST}/build/${appSlug}`);
}

default: {
return NextResponse.json(
{ message: "Invalid file extension, only .apk or .ipa are allowed" },
{ status: 400 }
);
}
const result = await adapter.saveArtifact(appName, artifact);
return NextResponse.json(result, { status: 201 });
} catch (err) {
const error = err as AdapterError;
return new Response(error.message, {
status: error.code || 500,
});
}
}
102 changes: 19 additions & 83 deletions src/app/build/[app_slug]/[platform]/manifest/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { UPLOAD_DIR } from "@/constants";
import path from "path";
import fs from "fs";

const HOST = process.env.HOST as string;
import { AdapterError } from "@/lib/adapters/Errors";
import { LocalFSAdapter } from "@/lib/adapters/LocalFSAdapter";

type Params = {
params: {
Expand All @@ -11,84 +9,22 @@ type Params = {
};
};

export function GET(request: Request, { params }: Params) {
const directory = path.join(UPLOAD_DIR, params.app_slug);

if (!fs.existsSync(directory)) {
return new Response("No Development build found", { status: 404 });
}

const files = fs.readdirSync(directory);

switch (params.platform) {
case "android": {
const manifest = files.find((file) => file.endsWith(".android.json"));
if (!manifest) {
return new Response("No Android development build found", {
status: 404,
});
}

const content = fs.readFileSync(path.join(directory, manifest));
return new Response(content, {
headers: { "Content-Type": "application/json" },
});
}

case "ios": {
const infoPlist = files.find((file) => file.endsWith(".ios.json"));

if (!infoPlist) {
return new Response("No iOS development build found", { status: 404 });
}

const content = fs
.readFileSync(path.join(directory, infoPlist))
.toString();

const info = JSON.parse(content);

const bundleIdentifier = info.CFBundleIdentifier;
const bundleVersion = info.CFBundleVersion;
const appName = info.CFBundleName;

const url = `${HOST}/build/${params.app_slug}/ios`;

const manifest = `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>${url}</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>bundle-identifier</key>
<string>${bundleIdentifier}</string>
<key>bundle-version</key>
<string>${bundleVersion}</string>
<key>kind</key>
<string>software</string>
<key>title</key>
<string>${appName}</string>
</dict>
</dict>
</array>
</dict>
</plist>`.trim();

return new Response(manifest, {
headers: { "Content-Type": "application/xml" },
});
}
export async function GET(request: Request, { params }: Params) {
try {
const adapter = new LocalFSAdapter({ uploadDir: UPLOAD_DIR });
const metadata = await adapter.getArtifactManifest(
params.app_slug,
params.platform
);
return new Response(metadata.content, {
headers: {
"Content-Type": metadata.type,
},
});
} catch (err) {
const error = err as AdapterError;
return new Response(error.message, {
status: error.code || 500,
});
}
}
65 changes: 19 additions & 46 deletions src/app/build/[app_slug]/[platform]/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { UPLOAD_DIR } from "@/constants";
import path from "path";
import fs from "fs";
import { AdapterError } from "@/lib/adapters/Errors";
import { LocalFSAdapter } from "@/lib/adapters/LocalFSAdapter";

type Params = {
params: {
Expand All @@ -9,49 +9,22 @@ type Params = {
};
};

export function GET(request: Request, { params }: Params) {
const directory = path.join(UPLOAD_DIR, params.app_slug);

if (!fs.existsSync(directory)) {
return new Response("No Development build found", { status: 404 });
}

const files = fs.readdirSync(directory);

const headers = new Headers();

headers.set("Content-Type", "application/octet-stream");

switch (params.platform) {
case "ios": {
const iosBuild = files.find((file) => file.endsWith(".ipa"));
if (!iosBuild) {
return new Response("No iOS development build found", { status: 404 });
}
const size = fs.statSync(path.join(directory, iosBuild)).size;
headers.set("Content-Disposition", `attachment; filename=${iosBuild}`);
headers.set("Content-Length", size.toString());
const content = fs.readFileSync(path.join(directory, iosBuild));

return new Response(content, { headers });
}
case "android": {
const androidBuild = files.find((file) => file.endsWith(".apk"));
if (!androidBuild) {
return new Response("No Android development build found", {
status: 404,
});
}
const size = fs.statSync(path.join(directory, androidBuild)).size;
headers.set(
"Content-Disposition",
`attachment; filename=${androidBuild}`
);
headers.set("Content-Length", size.toString());
const content = fs.readFileSync(path.join(directory, androidBuild));

return new Response(content, { headers });
}
export async function GET(request: Request, { params }: Params) {
try {
const headers = new Headers();
headers.set("Content-Type", "application/octet-stream");
const adapter = new LocalFSAdapter({ uploadDir: UPLOAD_DIR });
const metadata = await adapter.getArtifactFile(
params.app_slug,
params.platform
);
headers.set("Content-Disposition", `attachment; filename=${metadata.name}`);
headers.set("Content-Length", metadata.size.toString());
return new Response(metadata.content, { headers });
} catch (err) {
const error = err as AdapterError;
return new Response(error.message, {
status: error.code || 500,
});
}
return new Response("Hello worker!", { status: 200 });
}
Loading