src/services/photos.ts
Rama main · réplica de lectura del repositorio original.
import type { Ref } from 'vue'
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'
import { Filesystem } from '@capacitor/filesystem'
import mime from 'mime'
import { toast } from 'vue-sonner'
import { useMainStore } from '~/stores/main'
import { useOrganizationStore } from '~/stores/organization'
import { useConsole } from './console'
import { createSignedImageUrl } from './storage'
const capgo = useConsole()
const SIGNED_IMAGE_STORAGE_PATH_REGEX = /\/storage\/v1\/object\/(?:public\/|sign\/)?images\/(.+)$/
const LEADING_SLASHES_REGEX = /^\/+/
const IMAGES_PREFIX_REGEX = /^images\//
/** Receives the error message to show when a photo upload fails. */
export type PhotoErrorHandler = (message: string) => void
/**
* Either a callback that displays the error, or a legacy form id (ignored): the error is then shown as a toast.
*/
export type PhotoErrorTarget = string | PhotoErrorHandler
function reportPhotoError(target: PhotoErrorTarget, message: string) {
if (typeof target === 'function') {
target(message)
return
}
if (message)
toast.error(message)
}
function normalizeImageStoragePath(path?: string | null) {
if (!path)
return ''
const pathWithoutQuery = path.split('?')[0]
const signedUrlMatch = SIGNED_IMAGE_STORAGE_PATH_REGEX.exec(pathWithoutQuery)
if (signedUrlMatch?.[1])
return signedUrlMatch[1].replace(LEADING_SLASHES_REGEX, '')
return pathWithoutQuery.replace(IMAGES_PREFIX_REGEX, '').replace(LEADING_SLASHES_REGEX, '')
}
function getPhotoErrorMessage(error: unknown) {
if (typeof error === 'string')
return error
if (error instanceof Error)
return error.message
if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string')
return error.message
return ''
}
export function isPhotoSelectionCancelledError(error: unknown) {
const message = getPhotoErrorMessage(error).toLowerCase()
if (!message)
return false
return message.includes('user')
&& /cancel(?:led|ed)/.test(message)
&& /photos?|images?|camera|picker|selection|picking|app/.test(message)
}
function base64ToArrayBuffer(base64: string) {
const binary = atob(base64)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.codePointAt(i) ?? 0
}
return bytes.buffer
}
async function uploadPhotoShared(
data: string,
storagePath: string,
contentType: string,
isLoading: Ref<boolean>,
callback: (success: boolean, storagePath: string, signedUrl: string) => Promise<void>,
) {
const { error } = await capgo.storage
.from('images')
.upload(storagePath, base64ToArrayBuffer(data), {
contentType,
})
const signedUrl = error ? '' : await createSignedImageUrl(storagePath)
isLoading.value = false
if (error || !signedUrl)
await callback(false, '', '')
else
await callback(true, storagePath, signedUrl)
}
async function uploadPhotoUser(onError: PhotoErrorTarget, data: string, fileName: string, contentType: string, isLoading: Ref<boolean>, wentWrong: string) {
const main = useMainStore()
const userId = main.user?.id
if (!userId) {
reportPhotoError(onError, wentWrong)
console.error('No user id', userId)
return
}
const safeUserId = userId
async function userCallback(success: boolean, storagePath: string, signedUrl: string) {
if (!success) {
reportPhotoError(onError, wentWrong)
return
}
let previousImagePath = ''
const { data: currentUser, error: currentUserError } = await capgo
.from('users')
.select('image_url')
.eq('id', safeUserId)
.maybeSingle()
if (currentUserError)
console.error('cannot fetch current user image before update', currentUserError)
else
previousImagePath = normalizeImageStoragePath(currentUser?.image_url)
const { data: usr, error: dbError } = await capgo
.from('users')
.update({ image_url: storagePath })
.eq('id', safeUserId)
.select()
.single()
if (!usr || dbError) {
reportPhotoError(onError, wentWrong)
console.error('upload error', dbError)
const { error: cleanupUploadError } = await capgo
.storage
.from('images')
.remove([storagePath])
if (cleanupUploadError)
console.error('cannot cleanup newly uploaded user image after db error', cleanupUploadError)
return
}
if (previousImagePath && previousImagePath !== storagePath) {
const { error: deletePreviousImageError } = await capgo
.storage
.from('images')
.remove([previousImagePath])
if (deletePreviousImageError)
console.error('cannot delete previous user image', deletePreviousImageError)
}
usr.image_url = signedUrl
main.user = usr
}
await uploadPhotoShared(data, `${safeUserId}/${fileName}`, contentType, isLoading, userCallback)
}
async function uploadPhotoOrg(onError: PhotoErrorTarget, data: string, fileName: string, contentType: string, isLoading: Ref<boolean>, wentWrong: string) {
const organizationStore = useOrganizationStore()
const gid = organizationStore.currentOrganization?.gid
if (!gid) {
console.error('No current org id', gid)
reportPhotoError(onError, wentWrong)
return
}
const safeGid = gid
async function orgCallback(success: boolean, storagePath: string, _signedUrl: string) {
if (!success) {
reportPhotoError(onError, wentWrong)
return
}
const { data: usr, error: dbError } = await capgo
.from('orgs')
.update({ logo: storagePath })
.eq('id', safeGid)
.select('id')
.single()
if (!usr || dbError) {
reportPhotoError(onError, wentWrong)
console.error('upload error', dbError)
return
}
await organizationStore.fetchOrganizations()
organizationStore.setCurrentOrganization(usr.id)
}
await uploadPhotoShared(data, `org/${safeGid}/logo/${fileName}`, contentType, isLoading, orgCallback)
}
export async function uploadOrgLogoFile(orgId: string, file: Blob, fileName?: string) {
const organizationStore = useOrganizationStore()
const safeOrgId = orgId.trim()
if (!safeOrgId)
throw new Error('Organization ID is required')
const extension = mime.getExtension(file.type) ?? 'png'
const targetFileName = fileName ?? `${Date.now()}.${extension}`
const storagePath = `org/${safeOrgId}/logo/${targetFileName}`
const { error: uploadError } = await capgo.storage
.from('images')
.upload(storagePath, file, {
contentType: file.type || undefined,
upsert: true,
})
if (uploadError)
throw uploadError
const { data: updatedOrg, error: updateError } = await capgo
.from('orgs')
.update({ logo: storagePath })
.eq('id', safeOrgId)
.select('id')
.single()
if (updateError || !updatedOrg) {
const { error: cleanupError } = await capgo.storage
.from('images')
.remove([storagePath])
if (cleanupError)
console.error('cannot cleanup orphaned org logo upload', cleanupError)
throw updateError ?? new Error('Organization logo update affected no rows')
}
try {
await organizationStore.fetchOrganizations()
}
catch (error) {
console.error('Failed to refresh organizations after org logo upload', error)
}
organizationStore.setCurrentOrganization(safeOrgId)
return storagePath
}
function blobToData(blob: Blob) {
return new Promise<string>((resolve) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result as string)
reader.readAsDataURL(blob)
})
}
export async function takePhoto(onError: PhotoErrorTarget, isLoading: Ref<boolean>, type: 'org' | 'user', wentWrong: string) {
const uploadPhoto = (type === 'user') ? uploadPhotoUser : uploadPhotoOrg
let cameraPhoto
try {
cameraPhoto = await Camera.getPhoto({
resultType: CameraResultType.DataUrl,
source: CameraSource.Camera,
quality: 100,
})
}
catch (error) {
if (!isPhotoSelectionCancelledError(error))
console.error(error)
return
}
if (!cameraPhoto.dataUrl)
return
const contentType = mime.getType(cameraPhoto.format)
if (!contentType)
return
isLoading.value = true
const fileName = `${Date.now()}.${cameraPhoto.format}`
try {
await uploadPhoto(onError, cameraPhoto.dataUrl.split('base64,')[1], fileName, contentType, isLoading, wentWrong)
}
catch (e) {
console.error(e)
isLoading.value = false
}
}
export async function pickPhoto(onError: PhotoErrorTarget, isLoading: Ref<boolean>, type: 'org' | 'user', wentWrong: string) {
const uploadPhoto = (type === 'user') ? uploadPhotoUser : uploadPhotoOrg
let pickedImages
try {
pickedImages = await Camera.pickImages({
limit: 1,
quality: 100,
})
}
catch (error) {
if (!isPhotoSelectionCancelledError(error))
console.error(error)
return
}
const { photos } = pickedImages
if (photos.length === 0)
return
try {
let contents
if (photos[0].path) {
contents = await Filesystem.readFile({
path: photos[0].path ?? photos[0].webPath,
})
}
else {
const blob = await blobToData(await fetch(photos[0].webPath).then(r => r.blob()))
contents = { data: blob.split('base64,')[1] }
}
const contentType = mime.getType(photos[0].format)
if (!contentType)
return
isLoading.value = true
await uploadPhoto(
onError,
contents.data as any,
`${Date.now()}.${photos[0].format}`,
contentType,
isLoading,
wentWrong,
)
}
catch (e) {
console.error(e)
isLoading.value = false
}
}