import * as Sentry from "@sentry/react-native"; import axios from "axios"; import * as MediaLibrary from "expo-media-library"; import env from "../env"; import { client } from "../graphql/client"; import { INSERT_NEW_DOCUMENT } from "../graphql/documents.queries"; import { axiosAuthInterceptorId } from "./CleanAxios"; import { splitClient } from "../components/screen-main/screen-main.component"; //Context: currentUserEmail, bodyshop, jobid, invoiceid //Required to prevent headers from getting set and rejected from Cloudinary. var cleanAxios = axios.create(); cleanAxios.interceptors.request.eject(axiosAuthInterceptorId); export const handleUpload = async (ev, context) => { const { mediaId, onError, onSuccess, onProgress } = ev; const { bodyshop, jobId } = context; const imageData = await MediaLibrary.getAssetInfoAsync(mediaId); const newFile = await ( await fetch(imageData.localUri || imageData.uri) ).blob(); let extension = imageData.localUri.split(".").pop(); //Default to Cloudinary in case of split treatment errors. let destination = splitClient?.getTreatment("Imgproxy") === "on" ? "imgproxy" : "cloudinary"; let key = destination === "imgproxy" ? `${bodyshop.id}/${jobId}/${replaceAccents( imageData.filename || imageData.uri.split("/").pop() ).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.${extension}` : `${bodyshop.id}/${jobId}/${( imageData.filename || imageData.uri.split("/").pop() ).replace(/\.[^/.]+$/, "")}-${new Date().getTime()}`; const res = destination === "imgproxy" ? await uploadToImgproxy( key, mediaId, imageData, extension, newFile.type, //Filetype newFile, //File onError, onSuccess, onProgress, context ) : await uploadToCloudinary( key, mediaId, imageData, extension, newFile.type, //Filetype newFile, //File onError, onSuccess, onProgress, context ); return res; }; export const handleUploadImgproxy = async (ev, context) => { const { mediaId, onError, onSuccess, onProgress } = ev; const { bodyshop, jobId } = context; const imageData = await MediaLibrary.getAssetInfoAsync(mediaId); const newFile = await ( await fetch(imageData.localUri || imageData.uri) ).blob(); let extension = imageData.localUri.split(".").pop(); let key = `${bodyshop.id}/${jobId}/${( imageData.filename || imageData.uri.split("/").pop() ).replace(/\.[^/.]+$/, "")}-${new Date().getTime()}`; const res = await uploadToImgproxy( key, mediaId, imageData, extension, newFile.type, //Filetype newFile, //File onError, onSuccess, onProgress, context ); return res; }; export const uploadToImgproxy = async ( key, mediaId, imageData, extension, fileType, file, onError, onSuccess, onProgress, context ) => { const { bodyshop, jobId, uploaded_by } = context; //Get the signed url allowing us to PUT to S3. const signedURLResponse = await axios.post( `${env.API_URL}/media/imgproxy/sign`, { filenames: [key], bodyshopid: bodyshop.id, jobid: jobId, } ); if (signedURLResponse.status !== 200) { console.log("Error Getting Signed URL", signedURLResponse.statusText); if (onError) onError(signedURLResponse.statusText); return { success: false, error: signedURLResponse.statusText }; } const { presignedUrl: preSignedUploadUrlToS3, key: s3Key } = signedURLResponse.data.signedUrls[0]; var options = { headers: { "Content-Type": fileType, "Content-Length": file.size, }, transformRequest: [(data) => data], //Dave had this magical solution because Axios makes no sense. onUploadProgress: (e) => { if (onProgress) onProgress({ percent: (e.loaded / e.total) * 100 }); }, }; console.log("Uploading to S3", key, preSignedUploadUrlToS3, file, options); const formData = new FormData(); formData.append("file", { uri: imageData.localUri, type: fileType, name: file.data.name, }); try { const s3UploadResponse = await cleanAxios.put( preSignedUploadUrlToS3, file, options ); debugger; console.log(s3UploadResponse); } catch (error) { console.log("Error uploading to S3", error.message, error.stack); Sentry.Native.captureException(error); } const documentInsert = await client.mutate({ mutation: INSERT_NEW_DOCUMENT, variables: { docInput: [ { ...(jobId ? { jobid: jobId } : {}), uploaded_by: uploaded_by, key: s3Key, type: fileType, extension: extension, bodyshopid: bodyshop.id, size: file.size, ...(imageData.creationTime ? { takenat: new Date(imageData.creationTime) } : {}), }, ], }, }); if (!documentInsert.errors) { if (onSuccess) onSuccess({ uid: documentInsert.data.insert_documents.returning[0].id, name: documentInsert.data.insert_documents.returning[0].name, status: "done", key: documentInsert.data.insert_documents.returning[0].key, }); } else { if (onError) onError(JSON.stringify(documentInsert.errors)); return { success: false, error: JSON.stringify(documentInsert.errors), mediaId, }; } return { success: true, mediaId }; }; export const uploadToCloudinary = async ( key, mediaId, imageData, extension, fileType, file, onError, onSuccess, onProgress, context ) => { const { bodyshop, jobId, uploaded_by } = context; //Set variables for getting the signed URL. let timestamp = Math.floor(Date.now() / 1000); let public_id = key; const upload_preset = fileType.startsWith("video") ? "incoming_upload_video" : "incoming_upload"; //Get the signed url. let signedURLResponse; try { signedURLResponse = await axios.post(`${env.API_URL}/media/sign`, { public_id: public_id, timestamp: timestamp, upload_preset: upload_preset, }); } catch (error) { console.log("ERROR GETTING SIGNED URL", error); Sentry.Native.captureException(error); return { success: false, error: error }; } if (signedURLResponse.status !== 200) { console.log("Error Getting Signed URL", signedURLResponse.statusText); if (onError) onError(signedURLResponse.statusText); return { success: false, error: signedURLResponse.statusText }; } //Build request to end to cloudinary. var signature = signedURLResponse.data; var options = { headers: { "X-Requested-With": "XMLHttpRequest", "Content-Type": "multipart/form-data", }, onUploadProgress: (e) => { if (onProgress) onProgress({ percent: e.loaded / e.total, loaded: e.loaded }); }, }; const formData = new FormData(); formData.append("file", { uri: imageData.localUri, type: fileType, name: file.data.name, }); formData.append("upload_preset", upload_preset); formData.append("api_key", env.REACT_APP_CLOUDINARY_API_KEY); formData.append("public_id", public_id); formData.append("timestamp", timestamp); formData.append("signature", signature); //Upload request to Cloudinary let cloudinaryUploadResponse; try { cloudinaryUploadResponse = await cleanAxios.post( `${env.REACT_APP_CLOUDINARY_ENDPOINT_API}/${DetermineFileType( fileType )}/upload`, formData, options ); } catch (error) { console.log("CLOUDINARY error", error.response, cloudinaryUploadResponse); Sentry.Native.captureException(error); if (onError) onError(error.message); return { success: false, error: error }; } if (cloudinaryUploadResponse.status !== 200) { console.log( "Error uploading to cloudinary.", cloudinaryUploadResponse.statusText, cloudinaryUploadResponse ); if (onError) onError(cloudinaryUploadResponse.statusText); return { success: false, error: cloudinaryUploadResponse.statusText }; } //Insert the document with the matching key. const documentInsert = await client.mutate({ mutation: INSERT_NEW_DOCUMENT, variables: { docInput: [ { ...(jobId ? { jobid: jobId } : {}), uploaded_by: uploaded_by, key: key, type: fileType, extension: extension, bodyshopid: bodyshop.id, size: cloudinaryUploadResponse.data.bytes || file.size, ...(imageData.creationTime ? { takenat: new Date(imageData.creationTime) } : {}), }, ], }, }); if (!documentInsert.errors) { if (onSuccess) onSuccess({ uid: documentInsert.data.insert_documents.returning[0].id, name: documentInsert.data.insert_documents.returning[0].name, status: "done", key: documentInsert.data.insert_documents.returning[0].key, }); } else { if (onError) onError(JSON.stringify(documentInsert.errors)); return { success: false, error: JSON.stringify(documentInsert.errors), mediaId, }; } return { success: true, mediaId }; }; export function DetermineFileType(filetype) { if (!filetype) return "auto"; else if (filetype.startsWith("image")) return "image"; else if (filetype.startsWith("video")) return "video"; else if (filetype.startsWith("application/pdf")) return "image"; else if (filetype.startsWith("application")) return "raw"; return "auto"; } export function formatBytes(a, b = 2) { if (0 === a || !a) return "0 Bytes"; const c = 0 > b ? 0 : b, d = Math.floor(Math.log(a) / Math.log(1024)); return ( parseFloat((a / Math.pow(1024, d)).toFixed(c)) + " " + ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"][d] ); } function replaceAccents(str) { // Verifies if the String has accents and replace them if (str.search(/[\xC0-\xFF]/g) > -1) { str = str .replace(/[\xC0-\xC5]/g, "A") .replace(/[\xC6]/g, "AE") .replace(/[\xC7]/g, "C") .replace(/[\xC8-\xCB]/g, "E") .replace(/[\xCC-\xCF]/g, "I") .replace(/[\xD0]/g, "D") .replace(/[\xD1]/g, "N") .replace(/[\xD2-\xD6\xD8]/g, "O") .replace(/[\xD9-\xDC]/g, "U") .replace(/[\xDD]/g, "Y") .replace(/[\xDE]/g, "P") .replace(/[\xE0-\xE5]/g, "a") .replace(/[\xE6]/g, "ae") .replace(/[\xE7]/g, "c") .replace(/[\xE8-\xEB]/g, "e") .replace(/[\xEC-\xEF]/g, "i") .replace(/[\xF1]/g, "n") .replace(/[\xF2-\xF6\xF8]/g, "o") .replace(/[\xF9-\xFC]/g, "u") .replace(/[\xFE]/g, "p") .replace(/[\xFD\xFF]/g, "y"); } return str; }