Skip to content

Commit

Permalink
Add generate bundle apk from aab
Browse files Browse the repository at this point in the history
  • Loading branch information
yunusefendi52 committed Dec 8, 2024
1 parent d9de085 commit eb05500
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 31 deletions.
4 changes: 2 additions & 2 deletions cli/cli.mjs
Git LFS file not shown
27 changes: 21 additions & 6 deletions cli/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { uploadArtifact, updateMyFetchApiKey } from '../utils/upload-utils.js'
import { readFileSync } from "node:fs"
import { promises } from "node:fs"
import { resolve } from "node:path"
import { parseArgs } from "node:util"
import { extractAabToApk } from '~/server/utils/extract-aab-to-apk.js'

const args = parseArgs({
options: {
Expand Down Expand Up @@ -42,14 +43,28 @@ async function start() {
if (values.distribute) {
const { orgName, appName } = slugToOrgApp(values.slug!)
const filePath = resolve(values.file!)
const file = readFileSync(filePath)
const file = await promises.readFile(filePath)
console.log("Distributing", {
filePath,
})
await uploadArtifact(file, orgName, appName, values.releaseNotes ? values.releaseNotes : null, undefined)
console.log('Finished Distributing', {
filePath,
})
var bundleApkPath: string | undefined = undefined
var disposeBundle: (() => Promise<void>) | undefined
try {
if (filePath.endsWith('.aab')) {
const aabPath = filePath
const { bundleApk, dispose } = await extractAabToApk(aabPath)
bundleApkPath = bundleApk
disposeBundle = dispose
}
const bundleApkFile = bundleApkPath ? await promises.readFile(bundleApkPath) : undefined
await uploadArtifact(file, orgName, appName, values.releaseNotes ? values.releaseNotes : null, bundleApkFile)
console.log('Finished Distributing', {
filePath,
bundleApkPath,
})
} finally {
await disposeBundle?.()
}
} else {
console.error('No valid command')
}
Expand Down
11 changes: 4 additions & 7 deletions components/AppFileUpload.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ watchEffect(() => {
})
const { mutateAsync, isPending } = useMutation({
mutationFn: async (param: { file: File, apkFile: File | undefined }) => {
const { artifactId } = await onUpload(param.file, param.apkFile)
mutationFn: async (param: { file: File }) => {
const { artifactId } = await onUpload(param.file)
const groupIds = selectedGroup.value?.map(e => e.id) ?? []
if (artifactId && groupIds && groupIds.length) {
await $fetch('/api/update-artifact-groups', {
Expand All @@ -89,16 +89,13 @@ const submit = async () => {
if (!realFile) {
return
}
// const inputApkFile = fileApkRef.value as HTMLInputElement | null
// const apkActualFile = inputApkFile?.files && inputApkFile?.files.length ? inputApkFile.files[0] : undefined
mutateAsync({
file: realFile,
apkFile: undefined,
})
}
const onUpload = async (file: File, apkFile: File | undefined) => {
const data = await uploadArtifact(file, orgName.value, appName.value, releaseNotes.value, apkFile)
const onUpload = async (file: File) => {
const data = await uploadArtifact(file, orgName.value, appName.value, releaseNotes.value, 'generate_bundle')
return {
artifactId: data!.artifactId,
}
Expand Down
4 changes: 2 additions & 2 deletions composables/useDownloadArtifact.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export const useDownloadArtifact = (appName: string, orgName: string) => {
const isDownloading = ref(false)

const download = async (releaseId: string, publicId: string | undefined) => {
let url = `/api/artifacts/download-artifact?appName=${appName}&orgName=${orgName}&releaseId=${releaseId}&publicId=${publicId || ''}`
const download = async (releaseId: string, publicId: string | undefined, hasApk: boolean) => {
let url = `/api/artifacts/download-artifact?appName=${appName}&orgName=${orgName}&releaseId=${releaseId}&publicId=${publicId || ''}&hasApk=${hasApk}`
if (isIosDevice()) {
try {
isDownloading.value = true
Expand Down
5 changes: 5 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export default defineNuxtConfig({
S3_ENDPOINT: '',
S3_ACCESS_KEY_ID: '',
S3_SECRET_ACCESS_KEY: '',
BUNDLEAAB: {
KEYSTORE_BASE64: '',
KEYSTORE_PASS: '',
KEYSTORE_ALIAS: '',
},
public: {
GOOGLE_CLIENT_ID: '',
LOCAL_AUTH_ENABLED: false,
Expand Down
3 changes: 2 additions & 1 deletion pages/install/[publicId]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
<!-- <span>30mbbb</span> -->
</div>
<div>
<Button label="Download" @click="download(item.releaseId.toString(), publicId.toString())"
<Button label="Download"
@click="download(item.releaseId.toString(), publicId.toString(), true)"
:loading="isDownloading" />
</div>
</div>
Expand Down
12 changes: 8 additions & 4 deletions pages/orgs/[orgName]/apps/[appId]/releases/[detailArtifact].vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@
<ConfirmPopup></ConfirmPopup>
<!-- <div>{{ detailArtifact }}</div> -->
<div class="card flex flex-col gap-3 m-4">
<div class="flex flex-row items-center">
<div class="flex flex-col md:flex-row items-stretch gap-2">
<div class="flex flex-col gap-1 flex-1">
<span class="text-sm">Release Id {{ detailArtifact?.releaseId }}</span>
<span class="font-semibold text-xl">Version {{ detailArtifact?.versionName2 }} ({{
detailArtifact?.versionCode2
}})</span>
<span class="text-lg">{{ formatDate(detailArtifact?.createdAt) }}</span>
</div>
<div class="flex flex-col gap-2 items-stretch">
<Button :loading="isDownloading" label="Download"
@click="() => download(releaseId, undefined)"></Button>
<div class="flex flex-col gap-2 items-stretch md:items-end">
<div class="flex flex-col md:flex-row gap-2">
<Button :loading="isDownloading" label="Download AAB"
@click="() => download(releaseId, undefined, false)"></Button>
<Button :loading="isDownloading" label="Download APK" v-if="detailArtifact?.hasApk"
@click="() => download(releaseId, undefined, true)"></Button>
</div>
<Button :loading="isPending" @click="confirmDelete($event)" icon="pi pi-trash" label="Delete"
severity="danger" />
</div>
Expand Down
1 change: 1 addition & 0 deletions server/api/artifacts/detail-artifact.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export default defineEventHandler(async (event) => {
const response = {
...detailArtifact,
fileObjectKey: undefined,
hasApk: detailArtifact.fileObjectApkKey ? true : false,
fileMetadata: {
md5: headObject.etag,
contentLength: headObject.contentLength,
Expand Down
13 changes: 9 additions & 4 deletions server/api/artifacts/download-artifact.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const getArtifactFromInternal = async (
appName: string,
releaseId: string,
publicId: string,
hasApk: boolean,
) => {
const { app, org } = publicId
? await getArtifactGroupFromPublicIdOrUser(event, orgName, appName, publicId)
Expand All @@ -22,9 +23,11 @@ export const getArtifactFromInternal = async (
)
},
}).then(takeUniqueOrThrow)
const { assets } = getStorageKeys(app.organizationsId!, app.id, detailArtifact.fileObjectKey)
const actuallyHasApk = hasApk && (detailArtifact.fileObjectApkKey ? true : false)
const { assets } = getStorageKeys(app.organizationsId!, app.id, actuallyHasApk ? detailArtifact.fileObjectApkKey! : detailArtifact.fileObjectKey)
const s3 = new S3Fetch()
const signedUrl = await s3.getSignedUrlGetObject(assets, 1800, `attachment; filename ="${app.name}${detailArtifact.extension ? `.${detailArtifact.extension}` : ''}"`)
const artifactExt = actuallyHasApk ? 'apk' : detailArtifact.extension
const signedUrl = await s3.getSignedUrlGetObject(assets, 1800, `attachment; filename ="${app.name}${artifactExt ? `.${artifactExt}` : ''}"`)
return {
signedUrl,
userOrg: org,
Expand All @@ -34,19 +37,21 @@ export const getArtifactFromInternal = async (
}

export default defineEventHandler(async (event) => {
const { appName, orgName, releaseId, hasManifestPList, publicId } = await getValidatedQuery(event, z.object({
const { appName, orgName, releaseId, hasManifestPList, publicId, hasApk } = await getValidatedQuery(event, z.object({
appName: z.string().min(1),
orgName: z.string().min(1),
releaseId: z.string().min(1),
hasManifestPList: z.any(),
publicId: z.string().nullable(),
hasApk: z.string().transform(e => e === 'true'),
}).parse)
const { signedUrl, app, detailArtifact, } = await getArtifactFromInternal(
event,
orgName,
appName,
releaseId,
publicId || '')
publicId || '',
hasApk)
if (hasManifestPList) {
return {
signedUrl,
Expand Down
31 changes: 31 additions & 0 deletions server/api/artifacts/get-bundle-keystore.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export type BundleKeystoreResponse = {
appKeystorePass: string
appKeystoreAlias: string
appKeystoreBase64: string
}

export default defineEventHandler(async (event) => {
const { BUNDLEAAB: { KEYSTORE_BASE64, KEYSTORE_PASS, KEYSTORE_ALIAS } } = useRuntimeConfig(event)
const apiKey = getHeader(event, 'API-KEY')
if (apiKey) {
const { app: { apiAuthKey } } = useRuntimeConfig(event)
if (!apiAuthKey) {
throw createError({
message: 'Please provide using env NUXT_APP_API_AUTH_KEY to get key',
statusCode: 500,
})
}
await verifyToken(event, apiKey, apiAuthKey)
} else if (!event.context.auth) {
throw createError({
message: 'Invalid get auth key',
})
}

// setHeader(event, 'Cache-Control', 'max-age=86400, private')
return {
appKeystorePass: KEYSTORE_PASS,
appKeystoreAlias: KEYSTORE_ALIAS,
appKeystoreBase64: KEYSTORE_BASE64,
} satisfies BundleKeystoreResponse
})
87 changes: 87 additions & 0 deletions server/utils/extract-aab-to-apk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import util from 'util'
import { join, basename, extname } from 'path'
import { promises, createWriteStream, existsSync } from 'fs'
import child_process from 'node:child_process'
import { uuidv4 } from "uuidv7"
const exec = util.promisify(child_process.exec)
import { myFetch } from '../../utils/upload-utils.js'
import type { BundleKeystoreResponse } from '../api/artifacts/get-bundle-keystore.get.js'
import { Readable } from 'stream'
import { ReadableStream } from 'stream/web'
import { finished } from 'stream/promises'

function getDateFilename(): string {
const today = new Date()
const dd = String(today.getDate()).padStart(2, '0')
const mm = String(today.getMonth() + 1).padStart(2, '0')
const yyyy = today.getFullYear()

return `${yyyy}_${mm}_${dd}_${today.getHours()}_${today.getMinutes()}`
}

async function downloadFile(url: string, filepath: string) {
const response = await fetch(url);
const body = Readable.fromWeb(response.body as ReadableStream);
const tempFilepath = `${filepath}.tmp`
const download_write_stream = createWriteStream(tempFilepath);
await finished(body.pipe(download_write_stream));
await promises.rename(tempFilepath, filepath)
}

export async function extractAabToApk(aabPath: string) {
const cwdRoot = process.cwd()
const bundeFilesDir = join(cwdRoot, '.temp', 'bundle_files')
await promises.mkdir(bundeFilesDir, {
recursive: true,
})

const bundletoolJarPath = join(bundeFilesDir, 'bundletool.jar')
if (!existsSync(bundletoolJarPath)) {
await downloadFile('https://github.com/google/bundletool/releases/download/1.17.2/bundletool-all-1.17.2.jar', bundletoolJarPath)
}

const tempBundleDir = join(bundeFilesDir, `${getDateFilename()}_${uuidv4().replaceAll('-', '').slice(0, 12)}`)
// console.log('extract_aab: ', {
// tempBundleDir: tempBundleDir,
// })
await promises.mkdir(tempBundleDir, {
recursive: true,
})

const bundleApks = join(tempBundleDir, "bundle.apks")
const actualApk = join(tempBundleDir, `${basename(aabPath, extname(aabPath))}.apk`)
const keystoreFile = join(tempBundleDir, `app.jks`)

const bundleKeystoreResponse: BundleKeystoreResponse | undefined = await myFetch('/api/artifacts/get-bundle-keystore')
if (!bundleKeystoreResponse) {
throw 'Error bundleKeystoreResponse'
}
const keystorePass = bundleKeystoreResponse.appKeystorePass
const keystoreAlias = bundleKeystoreResponse.appKeystoreAlias
const keystoreBase64 = bundleKeystoreResponse.appKeystoreBase64
await promises.writeFile(keystoreFile, Buffer.from(keystoreBase64, 'base64'))

await exec(`
java -jar "${bundletoolJarPath}" build-apks --bundle=${aabPath} --output=${bundleApks} --mode=universal \\
--ks="${keystoreFile}" \\
--ks-pass=pass:${keystorePass} \\
--ks-key-alias=${keystoreAlias} \\
--key-pass=pass:${keystorePass}
`)
await exec(`unzip -p "${bundleApks}" universal.apk > ${actualApk}`)
await promises.rm(bundleApks, {
force: true,
})
// console.log('extract_aab: success', {
// output: actualApk,
// })
return {
bundleApk: actualApk,
dispose: async () => {
await promises.rm(tempBundleDir, {
force: true,
recursive: true,
})
},
}
}
18 changes: 13 additions & 5 deletions utils/upload-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,27 @@ export async function uploadArtifact(
redirect: "follow",
})
}
const generateBundleHeadless = fileApk === 'generate_bundle'
async function uploadApkUrl() {
if (fileApk) {
if (!apkUrl) {
console.error('Something happen apkUrl is null')
}
if (!apkUrl) {
console.error('Something happen apkUrl is null')
}
if (generateBundleHeadless) {

} else if (fileApk) {
await myFetch(apkUrl!.apkSignedUrl, {
method: 'put',
body: fileApk,
redirect: 'follow'
})
}
}
await Promise.all([uploadUrl(), uploadApkUrl()])
if (generateBundleHeadless) {
await uploadUrl()
await uploadApkUrl()
} else {
await Promise.all([uploadUrl(), uploadApkUrl()])
}
const data = await myFetch('/api/artifacts/upload-artifact-url', {
method: 'post',
body: {
Expand Down

0 comments on commit eb05500

Please sign in to comment.