diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts
index 402f6e0..c0e5729 100644
--- a/src/app/api/upload/route.ts
+++ b/src/app/api/upload/route.ts
@@ -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"];
@@ -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 | 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,
+ });
}
}
diff --git a/src/app/build/[app_slug]/[platform]/manifest/route.ts b/src/app/build/[app_slug]/[platform]/manifest/route.ts
index bb7c99e..80b092c 100644
--- a/src/app/build/[app_slug]/[platform]/manifest/route.ts
+++ b/src/app/build/[app_slug]/[platform]/manifest/route.ts
@@ -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: {
@@ -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 = `
-
-
- items
-
-
- assets
-
-
- kind
- software-package
- url
- ${url}
-
-
- metadata
-
- bundle-identifier
- ${bundleIdentifier}
- bundle-version
- ${bundleVersion}
- kind
- software
- title
- ${appName}
-
-
-
-
- `.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,
+ });
}
}
diff --git a/src/app/build/[app_slug]/[platform]/route.ts b/src/app/build/[app_slug]/[platform]/route.ts
index 02b46e8..88c1b07 100644
--- a/src/app/build/[app_slug]/[platform]/route.ts
+++ b/src/app/build/[app_slug]/[platform]/route.ts
@@ -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: {
@@ -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 });
}
diff --git a/src/app/build/[app_slug]/page.tsx b/src/app/build/[app_slug]/page.tsx
index e133dee..874ee5e 100644
--- a/src/app/build/[app_slug]/page.tsx
+++ b/src/app/build/[app_slug]/page.tsx
@@ -1,11 +1,7 @@
import { UPLOAD_DIR } from "@/constants";
-import path from "path";
-import fs from "fs";
import { notFound } from "next/navigation";
-import qrcode from "qrcode";
-import { Artifacts } from "@/types";
import { Builds } from "@/components/Builds";
-import { env } from "@/env";
+import { LocalFSAdapter } from "@/lib/adapters/LocalFSAdapter";
type Props = {
params: {
@@ -14,66 +10,12 @@ type Props = {
};
const getArtifacts = async (app_slug: string) => {
- const directory = path.join(UPLOAD_DIR, app_slug);
-
- if (!fs.existsSync(directory)) {
+ try {
+ const adapter = new LocalFSAdapter({ uploadDir: UPLOAD_DIR });
+ return await adapter.getArtifacts(app_slug);
+ } catch (error) {
return null;
}
-
- const artifacts: Artifacts = {
- android: null,
- ios: null,
- };
-
- const files = fs.readdirSync(directory);
-
- for await (const file of files) {
- const ext = path.extname(file).toLowerCase();
- const size = fs.statSync(path.join(directory, file)).size;
- const createdAt = fs.statSync(path.join(directory, file)).birthtime;
- switch (ext) {
- case ".apk": {
- const metadata = files.find((file) => file.endsWith(".android.json"));
- const metadataContent = fs
- .readFileSync(path.join(directory, metadata!))
- .toString();
-
- const qrCode = await qrcode.toDataURL(
- `${env.HOST}/build/${app_slug}/android`
- );
- artifacts.android = {
- downloadUrl: `/build/${app_slug}/android`,
- downloadQrCode: qrCode,
- size,
- uploadDate: createdAt.toISOString(),
- metadata: JSON.parse(metadataContent),
- };
- break;
- }
- case ".ipa": {
- const metadata = files.find((file) => file.endsWith(".ios.json"));
- const metadataContent = fs
- .readFileSync(path.join(directory, metadata!))
- .toString();
-
- const manifestUrl = `/build/${app_slug}/ios/manifest`;
- const manifestQrCodeUrl = `itms-services://?action=download-manifest&url=${env.HOST}${manifestUrl}`;
- const manifestQrCode = await qrcode.toDataURL(manifestQrCodeUrl);
- artifacts.ios = {
- downloadUrl: `/build/${app_slug}/ios`,
- downloadManifestUrl: manifestUrl,
- manifestQrCode: manifestQrCode,
- manifestQrCodeUrl,
- size,
- uploadDate: createdAt.toISOString(),
- metadata: JSON.parse(metadataContent),
- };
- break;
- }
- }
- }
-
- return artifacts;
};
export default async function Page({ params }: Props) {
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 9ed5e72..58e85b4 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -16,14 +16,10 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
+
-
+
{children}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index d3040ec..13717cc 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -5,15 +5,13 @@ import fs from "fs";
import { Badge } from "@/components/ui/badge";
import Link from "next/link";
import { env } from "@/env";
+import { LocalFSAdapter } from "@/lib/adapters/LocalFSAdapter";
export const dynamic = "force-dynamic";
const getArtifactNames = async () => {
- const files = await fs.promises.readdir(UPLOAD_DIR);
-
- return files.filter((maybeDir) =>
- fs.lstatSync(path.join(UPLOAD_DIR, maybeDir)).isDirectory()
- );
+ const adapter = new LocalFSAdapter({ uploadDir: UPLOAD_DIR });
+ return await adapter.getAllArtifactNames();
};
const getExampleCommand = () => {
@@ -28,7 +26,14 @@ export default async function Home() {
return (
-
+
Artifacts Repository
diff --git a/src/lib/adapters/AdapterBase.ts b/src/lib/adapters/AdapterBase.ts
new file mode 100644
index 0000000..cba7e09
--- /dev/null
+++ b/src/lib/adapters/AdapterBase.ts
@@ -0,0 +1,15 @@
+import { ArtifactFile, Artifacts, MetadataFile } from "@/types";
+
+export interface AdapterBase {
+ saveArtifact(name: string, file: File): Promise
;
+ getArtifacts(name: string): Promise;
+ getArtifactFile(
+ name: string,
+ platform: "ios" | "android"
+ ): Promise;
+ getArtifactManifest(
+ name: string,
+ platform: "ios" | "android"
+ ): Promise;
+ getAllArtifactNames(): Promise;
+}
diff --git a/src/lib/adapters/Errors.ts b/src/lib/adapters/Errors.ts
new file mode 100644
index 0000000..b5a623f
--- /dev/null
+++ b/src/lib/adapters/Errors.ts
@@ -0,0 +1,28 @@
+export class NotFoundError extends Error {
+ code: number = 404;
+ constructor(message: string) {
+ super(message);
+ this.name = "NotFoundError";
+ }
+}
+
+export class InvalidFileError extends Error {
+ code: number = 400;
+ constructor(message: string) {
+ super(message);
+ this.name = "InvalidFileError";
+ }
+}
+
+export class NotSupportedPlatformError extends Error {
+ code: number = 400;
+ constructor(message: string) {
+ super(message);
+ this.name = "NotSupportedPlatformError";
+ }
+}
+
+export type AdapterError =
+ | NotFoundError
+ | InvalidFileError
+ | NotSupportedPlatformError;
diff --git a/src/lib/adapters/LocalFSAdapter.ts b/src/lib/adapters/LocalFSAdapter.ts
new file mode 100644
index 0000000..c751b1b
--- /dev/null
+++ b/src/lib/adapters/LocalFSAdapter.ts
@@ -0,0 +1,332 @@
+import { AdapterBase } from "./AdapterBase";
+import slugify from "slugify";
+import path from "path";
+import fs from "fs";
+import JSZip from "jszip";
+import { ManifestParser } from "@/lib/extract-tools/manifest";
+import { env } from "@/env";
+import { parsePlist } from "../extract-tools/plist-parse";
+import { ArtifactFile, Artifacts, MetadataFile } from "@/types";
+import qrcode from "qrcode";
+import {
+ InvalidFileError,
+ NotFoundError,
+ NotSupportedPlatformError,
+} from "./Errors";
+
+type LocalFSAdapterOptions = {
+ uploadDir: string;
+};
+export class LocalFSAdapter implements AdapterBase {
+ private options: LocalFSAdapterOptions;
+
+ constructor(options: LocalFSAdapterOptions) {
+ this.options = options;
+ }
+
+ async saveArtifact(name: string, file: File): Promise {
+ const extension = file.name.split(".").pop();
+ const appSlug = `${slugify(name, {
+ lower: true,
+ remove: /[*+~.()'"!:@\/]/g,
+ })}`;
+
+ const artifactFile = await file.arrayBuffer();
+ const artifactBuffer = Buffer.from(artifactFile);
+
+ const dirPath = path.join(this.options.uploadDir, 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) {
+ throw new InvalidFileError(
+ "Invalid APK file, no AndroidManifest.xml found"
+ );
+ }
+
+ 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) {
+ throw new InvalidFileError(
+ "Invalid APK file, no versionCode or package name found"
+ );
+ }
+
+ // 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 `${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) {
+ throw new InvalidFileError("Invalid IPA file, no Info.plist found");
+ }
+
+ const plist = parsePlist(rawInfoPlist) as
+ | Record
+ | undefined;
+
+ if (typeof plist !== "object") {
+ throw new InvalidFileError(
+ "Invalid IPA file, Info.plist is not a valid plist"
+ );
+ }
+
+ const version = plist.CFBundleVersion as string | undefined;
+ const bundleId = plist.CFBundleIdentifier as string | undefined;
+
+ if (!version || !bundleId) {
+ throw new InvalidFileError(
+ "Invalid IPA file, no CFBundleVersion or CFBundleIdentifier found"
+ );
+ }
+
+ // 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 `${env.HOST}/build/${appSlug}`;
+ }
+
+ default: {
+ throw new NotSupportedPlatformError("Invalid file extension");
+ }
+ }
+ }
+
+ async getArtifacts(name: string): Promise {
+ const directory = path.join(this.options.uploadDir, name);
+
+ if (!fs.existsSync(directory)) {
+ throw new NotFoundError("No Development build found");
+ }
+
+ const artifacts: Artifacts = {
+ android: null,
+ ios: null,
+ };
+
+ const files = fs.readdirSync(directory);
+
+ for await (const file of files) {
+ const ext = path.extname(file).toLowerCase();
+ const size = fs.statSync(path.join(directory, file)).size;
+ const createdAt = fs.statSync(path.join(directory, file)).birthtime;
+ switch (ext) {
+ case ".apk": {
+ const metadata = files.find((file) => file.endsWith(".android.json"));
+ const metadataContent = fs
+ .readFileSync(path.join(directory, metadata!))
+ .toString();
+
+ const qrCode = await qrcode.toDataURL(
+ `${env.HOST}/build/${name}/android`
+ );
+ artifacts.android = {
+ downloadUrl: `/build/${name}/android`,
+ downloadQrCode: qrCode,
+ size,
+ uploadDate: createdAt.toISOString(),
+ metadata: JSON.parse(metadataContent),
+ };
+ break;
+ }
+ case ".ipa": {
+ const metadata = files.find((file) => file.endsWith(".ios.json"));
+ const metadataContent = fs
+ .readFileSync(path.join(directory, metadata!))
+ .toString();
+
+ const manifestUrl = `/build/${name}/ios/manifest`;
+ const manifestQrCodeUrl = `itms-services://?action=download-manifest&url=${env.HOST}${manifestUrl}`;
+ const manifestQrCode = await qrcode.toDataURL(manifestQrCodeUrl);
+ artifacts.ios = {
+ downloadUrl: `/build/${name}/ios`,
+ downloadManifestUrl: manifestUrl,
+ manifestQrCode: manifestQrCode,
+ manifestQrCodeUrl,
+ size,
+ uploadDate: createdAt.toISOString(),
+ metadata: JSON.parse(metadataContent),
+ };
+ break;
+ }
+ }
+ }
+
+ return artifacts;
+ }
+
+ getArtifactFile(
+ name: string,
+ platform: "ios" | "android"
+ ): Promise {
+ const directory = path.join(this.options.uploadDir, name);
+
+ if (!fs.existsSync(directory)) {
+ throw new NotFoundError("No Development build found");
+ }
+
+ const files = fs.readdirSync(directory);
+
+ switch (platform) {
+ case "ios": {
+ const iosBuild = files.find((file) => file.endsWith(".ipa"));
+ if (!iosBuild) {
+ throw new Error("No iOS development build found");
+ }
+ const size = fs.statSync(path.join(directory, iosBuild)).size;
+
+ const content = fs.readFileSync(path.join(directory, iosBuild));
+
+ return Promise.resolve({ content, size, name: iosBuild });
+ }
+ case "android": {
+ const androidBuild = files.find((file) => file.endsWith(".apk"));
+ if (!androidBuild) {
+ throw new Error("No Android development build found");
+ }
+ const size = fs.statSync(path.join(directory, androidBuild)).size;
+
+ const content = fs.readFileSync(path.join(directory, androidBuild));
+
+ return Promise.resolve({ content, size, name: androidBuild });
+ }
+ }
+ }
+
+ getArtifactManifest(
+ name: string,
+ platform: "ios" | "android"
+ ): Promise {
+ const directory = path.join(this.options.uploadDir, name);
+
+ if (!fs.existsSync(directory)) {
+ throw new NotFoundError("No Development build found");
+ }
+
+ const files = fs.readdirSync(directory);
+
+ switch (platform) {
+ case "android": {
+ const manifest = files.find((file) => file.endsWith(".android.json"));
+ if (!manifest) {
+ throw new NotFoundError("No Android development build found");
+ }
+
+ const content = fs.readFileSync(path.join(directory, manifest));
+ return Promise.resolve({ content, type: "application/json" });
+ }
+
+ case "ios": {
+ const infoPlist = files.find((file) => file.endsWith(".ios.json"));
+
+ if (!infoPlist) {
+ throw new NotFoundError("No iOS development build found");
+ }
+
+ 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 = `${env.HOST}/build/${name}/ios`;
+
+ const manifest = `
+
+
+ items
+
+
+ assets
+
+
+ kind
+ software-package
+ url
+ ${url}
+
+
+ metadata
+
+ bundle-identifier
+ ${bundleIdentifier}
+ bundle-version
+ ${bundleVersion}
+ kind
+ software
+ title
+ ${appName}
+
+
+
+
+ `.trim();
+
+ return Promise.resolve({
+ content: Buffer.from(manifest),
+ type: "application/xml",
+ });
+ }
+ }
+ }
+
+ async getAllArtifactNames() {
+ const files = await fs.promises.readdir(this.options.uploadDir);
+
+ return files.filter((maybeDir) =>
+ fs.lstatSync(path.join(this.options.uploadDir, maybeDir)).isDirectory()
+ );
+ }
+}
diff --git a/src/types.ts b/src/types.ts
index 4bc72e3..9220686 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -20,3 +20,14 @@ export type Artifacts = {
android: AndroidArtifact | null;
ios: IOSArtifact | null;
};
+
+export type ArtifactFile = {
+ name: string;
+ size: number;
+ content: Buffer;
+};
+
+export type MetadataFile = {
+ type: string;
+ content: Buffer;
+};