Initial commit of akmon project
This commit is contained in:
293
utils/audioUploadService.uts
Normal file
293
utils/audioUploadService.uts
Normal file
@@ -0,0 +1,293 @@
|
||||
import { AkReqResponse } from '@/uni_modules/ak-req/index.uts'
|
||||
import { SUPA_URL } from '@/ak/config.uts'
|
||||
import supa, { supaReady } from '@/components/supadb/aksupainstance.uts'
|
||||
import { AkSupaStorageUploadBuilder } from '@/components/supadb/aksupa.uts'
|
||||
|
||||
export type UploadAudioResult = {
|
||||
success : boolean
|
||||
url : string | null
|
||||
error ?: string | null
|
||||
durationMs ?: number | null
|
||||
mime ?: string | null
|
||||
sizeBytes ?: number | null
|
||||
bucket ?: string | null
|
||||
objectPath ?: string | null
|
||||
}
|
||||
|
||||
export type UploadAudioOptions = {
|
||||
filename ?: string
|
||||
mime ?: string
|
||||
bucket ?: string
|
||||
folder ?: string
|
||||
userId ?: string
|
||||
upsert ?: boolean
|
||||
cacheControlSeconds ?: number
|
||||
objectPath ?: string
|
||||
metadata ?: UTSJSONObject | null
|
||||
}
|
||||
|
||||
export class AudioUploadService {
|
||||
private static readonly DEFAULT_BUCKET = 'chat-audio'
|
||||
private static readonly DEFAULT_FOLDER = 'voices/app'
|
||||
|
||||
private static async ensureSession() : Promise<void> {
|
||||
try {
|
||||
const ready = await supaReady
|
||||
if (!ready) {
|
||||
console.warn('Supabase session not established for audio upload')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Supabase session initialization error', err)
|
||||
}
|
||||
}
|
||||
|
||||
private static normalizeFilename(filename : string, mime : string) : string {
|
||||
let base = filename
|
||||
if (base == null || base.trim().length === 0) {
|
||||
base = 'audio_' + Date.now()
|
||||
}
|
||||
base = base.trim()
|
||||
let cleaned = ''
|
||||
for (let i : Int = 0; i < base.length; i++) {
|
||||
const ch = base.charAt(i)
|
||||
const code = base.charCodeAt(i)
|
||||
if (code == null) continue
|
||||
const isDigit = code >= 48 && code <= 57
|
||||
const isUpper = code >= 65 && code <= 90
|
||||
const isLower = code >= 97 && code <= 122
|
||||
const isAllowedSymbol = (ch === '-' || ch === '_' || ch === '.' )
|
||||
if (isDigit || isUpper || isLower || isAllowedSymbol) {
|
||||
cleaned = cleaned + ch
|
||||
} else if (ch === ' ') {
|
||||
cleaned = cleaned + '_'
|
||||
}
|
||||
}
|
||||
if (cleaned.length === 0) {
|
||||
cleaned = 'audio_' + Date.now()
|
||||
}
|
||||
if (cleaned.indexOf('.') < 0) {
|
||||
cleaned = cleaned + AudioUploadService.extensionForMime(mime)
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
|
||||
private static extensionForMime(mime : string) : string {
|
||||
if (mime === 'audio/mpeg' || mime === 'audio/mp3') return '.mp3'
|
||||
if (mime === 'audio/wav') return '.wav'
|
||||
if (mime === 'audio/ogg') return '.ogg'
|
||||
if (mime === 'audio/aac') return '.aac'
|
||||
return '.mp3'
|
||||
}
|
||||
|
||||
private static sanitizeSegment(value : string) : string {
|
||||
let result = ''
|
||||
for (let i : Int = 0; i < value.length; i++) {
|
||||
const ch = value.charAt(i)
|
||||
const code = value.charCodeAt(i)
|
||||
if (code == null) continue
|
||||
const isDigit = code >= 48 && code <= 57
|
||||
const isUpper = code >= 65 && code <= 90
|
||||
const isLower = code >= 97 && code <= 122
|
||||
const isAllowedSymbol = (ch === '-' || ch === '_' || ch === '.')
|
||||
if (isDigit || isUpper || isLower || isAllowedSymbol) {
|
||||
result = result + ch
|
||||
}
|
||||
}
|
||||
if (result.length === 0) return 'segment'
|
||||
return result
|
||||
}
|
||||
|
||||
private static splitSegments(input : string) : Array<string> {
|
||||
const segments = new Array<string>()
|
||||
if (input == null) return segments
|
||||
const parts = input.split('/')
|
||||
for (let i : Int = 0; i < parts.length; i++) {
|
||||
const part = parts[i]
|
||||
if (part == null) continue
|
||||
const trimmed = part.trim()
|
||||
if (trimmed.length === 0) continue
|
||||
segments.push(AudioUploadService.sanitizeSegment(trimmed))
|
||||
}
|
||||
return segments
|
||||
}
|
||||
|
||||
private static formatTwoDigits(value : number) : string {
|
||||
if (value < 10) {
|
||||
return '0' + value
|
||||
}
|
||||
return value.toString()
|
||||
}
|
||||
|
||||
private static buildDateSegments(date : Date) : Array<string> {
|
||||
const segments = new Array<string>()
|
||||
segments.push(date.getFullYear().toString())
|
||||
segments.push(AudioUploadService.formatTwoDigits(date.getMonth() + 1))
|
||||
segments.push(AudioUploadService.formatTwoDigits(date.getDate()))
|
||||
return segments
|
||||
}
|
||||
|
||||
private static normalizeObjectPath(path : string, filename : string) : string {
|
||||
const trimmed = path.trim()
|
||||
if (trimmed.length === 0) {
|
||||
return filename
|
||||
}
|
||||
const useFolder = trimmed.endsWith('/') ? trimmed : trimmed + '/'
|
||||
const folderSegments = AudioUploadService.splitSegments(useFolder)
|
||||
folderSegments.push(filename)
|
||||
return folderSegments.join('/')
|
||||
}
|
||||
|
||||
private static buildObjectPath(filename : string, opts : UploadAudioOptions | null) : string {
|
||||
if (opts != null && opts.objectPath != null && opts.objectPath.trim().length > 0) {
|
||||
return AudioUploadService.normalizeObjectPath(opts.objectPath!!, filename)
|
||||
}
|
||||
const folderValue = opts != null && opts.folder != null ? opts.folder!! : AudioUploadService.DEFAULT_FOLDER
|
||||
const segments = AudioUploadService.splitSegments(folderValue)
|
||||
if (opts != null && opts.userId != null && opts.userId.trim().length > 0) {
|
||||
segments.push('users')
|
||||
segments.push(AudioUploadService.sanitizeSegment(opts.userId!!))
|
||||
}
|
||||
const dateSegments = AudioUploadService.buildDateSegments(new Date())
|
||||
for (let i : Int = 0; i < dateSegments.length; i++) {
|
||||
segments.push(dateSegments[i])
|
||||
}
|
||||
segments.push(filename)
|
||||
return segments.join('/')
|
||||
}
|
||||
|
||||
private static buildPublicUrl(bucket : string, objectPath : string) : string {
|
||||
let base = SUPA_URL
|
||||
if (base.endsWith('/')) {
|
||||
base = base.substring(0, base.length - 1)
|
||||
}
|
||||
let normalizedPath = objectPath
|
||||
while (normalizedPath.startsWith('/')) {
|
||||
normalizedPath = normalizedPath.substring(1)
|
||||
}
|
||||
return base + '/storage/v1/object/public/' + bucket + '/' + normalizedPath
|
||||
}
|
||||
|
||||
private static mergeJson(target : UTSJSONObject, source : UTSJSONObject) {
|
||||
const keys = UTSJSONObject.keys(source)
|
||||
for (let i : Int = 0; i < keys.length; i++) {
|
||||
const key = keys[i]
|
||||
target[key] = source.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
private static async fetchFileSize(filePath : string) : Promise<number | null> {
|
||||
return await new Promise<number | null>((resolve) => {
|
||||
let settled = false
|
||||
const finish = (value : number | null) => {
|
||||
if (settled) {
|
||||
return
|
||||
}
|
||||
settled = true
|
||||
resolve(value)
|
||||
}
|
||||
try {
|
||||
const fs = uni.getFileSystemManager()
|
||||
fs.getFileInfo({
|
||||
filePath,
|
||||
success: (res ) => {
|
||||
if (res != null && typeof res.size === 'number') {
|
||||
finish(res.size as number)
|
||||
return
|
||||
}
|
||||
finish(null)
|
||||
},
|
||||
fail: (_err) => {
|
||||
fs.stat({
|
||||
path: filePath,
|
||||
recursive: false,
|
||||
success: (statRes ) => {
|
||||
let size : number | null = null
|
||||
const stats = statRes != null ? statRes.stats as Array<any> : null
|
||||
if (stats != null) {
|
||||
for (let i : Int = 0; i < stats.length; i++) {
|
||||
const entry = stats[i]
|
||||
if (entry == null) {
|
||||
continue
|
||||
}
|
||||
const candidate = (entry as UTSJSONObject).get('size') as number | null
|
||||
if (candidate != null) {
|
||||
size = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
finish(size)
|
||||
},
|
||||
fail: () => finish(null)
|
||||
} as StatOptions)
|
||||
}
|
||||
} as GetFileInfoOptions)
|
||||
} catch (_fatal) {
|
||||
finish(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static async uploadAudio(filePath : string, opts ?: UploadAudioOptions) : Promise<UploadAudioResult> {
|
||||
if (filePath == null || filePath.length === 0) {
|
||||
return { success: false, url: null, error: 'invalid file path' }
|
||||
}
|
||||
await AudioUploadService.ensureSession()
|
||||
const mime = (opts != null && opts.mime != null && opts.mime.trim().length > 0) ? opts.mime!! : 'audio/mpeg'
|
||||
const filenameInput = opts != null && opts.filename != null ? opts.filename!! : ''
|
||||
const filename = AudioUploadService.normalizeFilename(filenameInput, mime)
|
||||
const bucket = (opts != null && opts.bucket != null && opts.bucket.trim().length > 0) ? opts.bucket!! : AudioUploadService.DEFAULT_BUCKET
|
||||
const objectPath = AudioUploadService.buildObjectPath(filename, opts ?? null)
|
||||
const uploadOptions = {} as UTSJSONObject
|
||||
const cacheSeconds = (opts != null && typeof opts.cacheControlSeconds === 'number') ? opts.cacheControlSeconds!! : 3600
|
||||
uploadOptions['cacheControl'] = cacheSeconds.toString()
|
||||
uploadOptions['contentType'] = mime
|
||||
if (opts != null && opts.upsert === true) {
|
||||
uploadOptions['x-upsert'] = 'true'
|
||||
}
|
||||
if (opts != null && opts.metadata != null) {
|
||||
AudioUploadService.mergeJson(uploadOptions, opts.metadata!!)
|
||||
}
|
||||
let response : AkReqResponse<any>
|
||||
try {
|
||||
const builder = new AkSupaStorageUploadBuilder(supa, bucket)
|
||||
response = await builder
|
||||
.path(objectPath)
|
||||
.file(filePath)
|
||||
.options(uploadOptions)
|
||||
.upload()
|
||||
} catch (err) {
|
||||
const message = (err instanceof Error) ? err.message : 'upload failed'
|
||||
return { success: false, url: null, error: message }
|
||||
}
|
||||
if (!(response.status >= 200 && response.status < 300)) {
|
||||
return { success: false, url: null, error: 'upload failed', mime }
|
||||
}
|
||||
let finalKey = objectPath
|
||||
const data = response.data
|
||||
if (data instanceof UTSJSONObject) {
|
||||
const key = data.getString('Key')
|
||||
if (key != null && key.length > 0) {
|
||||
finalKey = key
|
||||
}
|
||||
} else if (data != null && typeof data === 'object') {
|
||||
const jsonData = data as UTSJSONObject
|
||||
const keyValue = jsonData.getString('Key')
|
||||
if (keyValue != null && keyValue.length > 0) {
|
||||
finalKey = keyValue
|
||||
}
|
||||
}
|
||||
const publicUrl = AudioUploadService.buildPublicUrl(bucket, finalKey)
|
||||
const sizeBytes = await AudioUploadService.fetchFileSize(filePath)
|
||||
return {
|
||||
success: true,
|
||||
url: publicUrl,
|
||||
mime,
|
||||
sizeBytes,
|
||||
bucket,
|
||||
objectPath: finalKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AudioUploadService
|
||||
Reference in New Issue
Block a user