Files
imexmobile/redux/photos/photos.sagas.js
2025-10-14 14:13:30 -07:00

283 lines
8.6 KiB
JavaScript

import axios from "axios";
import Constants from "expo-constants";
import * as ImagePicker from "expo-image-picker";
import moment from 'moment';
import { all, call, delay, fork, put, select, takeEvery, takeLatest } from "redux-saga/effects";
import env from "../../env";
import { client } from '../../graphql/client';
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
import { axiosAuthInterceptorId } from "../../util/CleanAxios";
import { fetchImageFromUri, replaceAccents } from '../../util/uploadUtils';
import { selectBodyshop, selectCurrentUser } from "../user/user.selectors";
import {
mediaUploadCompleted,
mediaUploadFailure,
mediaUploadProgressBulk,
mediaUploadProgressOne,
mediaUploadStart,
mediaUploadSuccessOne
} from "./photos.actions";
import PhotosActionTypes from "./photos.types";
//Required to prevent headers from getting set and rejected from Cloudinary.
let cleanAxios = axios.create();
cleanAxios.interceptors.request.eject(axiosAuthInterceptorId);
export function* onOpenImagePicker() {
yield takeLatest(PhotosActionTypes.OPEN_IMAGE_PICKER, openImagePickerAction);
}
export function* openImagePickerAction({ payload: jobid }) {
try {
if (Constants.platform.ios) {
const cameraRollStatus =
yield ImagePicker.requestMediaLibraryPermissionsAsync();
const cameraStatus = yield ImagePicker.requestCameraPermissionsAsync();
if (
cameraRollStatus.status !== "granted" ||
cameraStatus.status !== "granted"
) {
alert("Sorry, we need these permissions to make this work!");
return;
}
}
let result = yield ImagePicker.launchImageLibraryAsync({
mediaTypes: ["images", "videos"],
aspect: [4, 3],
quality: 1,
allowsMultipleSelection: true,
exif: true,
});
if (!(result.canceled)) {
yield put(mediaUploadStart({ photos: result.assets, jobid }));
}
} catch (error) {
console.log("Saga Error: open Picker", error);
}
}
export function* onMediaUploadStart() {
yield takeLatest(PhotosActionTypes.MEDIA_UPLOAD_START, mediaUploadStartAction);
}
export function* mediaUploadStartAction({ payload: { photos, jobid } }) {
try {
console.log("Starting upload for", photos.length, "photos");
const bodyshop = yield select(selectBodyshop);
if (bodyshop.uselocalmediaserver) {
yield call(uploadToLocalMediaServer, photos, bodyshop, jobid);
}
else {
// Process photos in batches to avoid overwhelming the system
const batchSize = 3; // Upload 3 photos concurrently
const batches = [];
for (let i = 0; i < photos.length; i += batchSize) {
batches.push(photos.slice(i, i + batchSize));
}
// Process each batch sequentially, but photos within batch concurrently
for (const batch of batches) {
const uploadTasks = batch.map((photo, index) =>
fork(uploadSinglePhoto, photo, bodyshop, index, jobid)
);
// Wait for current batch to complete before starting next batch
yield all(uploadTasks);
// Small delay between batches to prevent overwhelming the server
yield delay(100);
}
}
yield put(mediaUploadCompleted());
} catch (error) {
console.log("Saga Error: upload start", error, error.stack);
yield put(mediaUploadFailure(error.message));
}
}
function* uploadSinglePhoto(photo, bodyshop, index, jobid) {
try {
yield put(mediaUploadProgressOne({ ...photo, status: 'starting', progress: 0 }));
const photoBlob = yield fetchImageFromUri(photo.uri); //has a name, and type.
const extension = photoBlob.data?.name?.split('.').pop();
const key = `${bodyshop.id}/${jobid}/${replaceAccents(
photoBlob.data.name
).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.${extension}`
yield call(uploadToImageProxy, photo, photoBlob, extension, key, bodyshop, jobid);
yield put(mediaUploadSuccessOne(photo));
} catch (error) {
console.log(`Upload failed for photo ${photo.uri}:`, error);
yield put(mediaUploadFailure({ ...photo, status: "error", error: error.message }));
}
}
function* uploadToLocalMediaServer(photos, bodyshop, jobid) {
try {
const options = {
headers: {
"Content-Type": "multipart/form-data",
ims_token: bodyshop.localmediatoken,
},
onUploadProgress: (e) => {
put(mediaUploadProgressBulk({ progress: e.loaded / e.total, loaded: e.loaded }));
},
};
const formData = new FormData();
formData.append("jobid", jobid);
for (const file of photos) {
formData.append("file", {
uri: file.uri,
type: file.mimeType,
name: file.fileName,
});
}
formData.append("skip_thumbnail", true);
try {
const imexMediaServerResponse = yield call(axios.post,
`${bodyshop.localmediaserverhttp}/jobs/upload`,
formData,
options
);
if (imexMediaServerResponse.status !== 200) {
console.log("Error uploading documents:", JSON.stringify(imexMediaServerResponse, null, 2));
} else {
// onSuccess({
// duration: imexMediaServerResponse.headers["x-response-time"],
// });
}
} catch (error) {
console.log("Error uploading documents:", error.message, JSON.stringify(error, null, 2));
}
} catch (error) {
console.log("Uncaught error", error);
}
}
function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid) {
try {
yield put(mediaUploadProgressOne({ ...photo, status: 'uploading', }));
//Get the signed url allowing us to PUT to S3.
const signedURLResponse = yield call(axios.post,
`${env.API_URL}/media/imgproxy/sign`,
{
filenames: [key],
bodyshopid: bodyshop.id,
jobid,
}
);
if (signedURLResponse.status !== 200) {
console.log("Error Getting Signed URL", signedURLResponse.statusText);
throw new Error(`Error getting signed URL : ${signedURLResponse.statusText}`);
}
const { presignedUrl: preSignedUploadUrlToS3, key: s3Key } =
signedURLResponse.data.signedUrls[0];
let uploadResult
try {
uploadResult = yield new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("PUT", preSignedUploadUrlToS3);
xhr.setRequestHeader("Content-Type", photoBlob.type);
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
put(mediaUploadProgressOne({ ...photo, progress: e.loaded / e.total, loaded: e.loaded }));
}
};
xhr.onload = () => {
if (xhr.status === 200) {
resolve(true);
} else {
reject(new Error(`Upload failed: ${xhr.statusText}`));
}
};
xhr.onerror = (req, event) => {
reject(new Error("Network error"));
};
xhr.send(photoBlob);
});
} catch (error) {
console.log("Error uploading to S3", error.message, error.stack);
throw error;
}
if (uploadResult) {
//Create doc record.
const uploaded_by = yield select(selectCurrentUser);
const [date, time] = photo.exif?.DateTime?.split(' ') || [];
const [year, month, day] = date ? date.split(':') : [];
const [hours, minutes, seconds] = time ? time.split(':') : [];
const pictureMoment = moment(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}`);
yield call(client.mutate, ({
mutation: INSERT_NEW_DOCUMENT,
variables: {
docInput: [
{
...(jobid ? { jobid: jobid } : {}),
uploaded_by: uploaded_by.email,
key: s3Key,
type: photoBlob.type,
extension: extension,
bodyshopid: bodyshop.id,
size: photoBlob.size,
...(photo.exif?.DateTime //TODO :Need to find how to do this.
? { takenat: pictureMoment }
: {}),
},
],
},
}));
}
} catch (error) {
console.log("Error uploading to image proxy", JSON.stringify(error));
throw new Error(`Image proxy upload failed: ${error.message}`);
}
}
// Handle cancellation of uploads
export function* onCancelUpload() {
yield takeEvery(PhotosActionTypes.CANCEL_UPLOAD, cancelUploadAction);
}
function* cancelUploadAction({ payload: photoId }) {
// const task = uploadTasks.get(photoId);
// if (task) {
// yield cancel(task);
// uploadTasks.delete(photoId);
// yield put(mediaUploadFailure({ photoId, error: 'Upload cancelled' }));
// }
}
export function* photosSagas() {
yield all([
call(onOpenImagePicker),
call(onMediaUploadStart),
//call(onCancelUpload)
]);
}