283 lines
8.6 KiB
JavaScript
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)
|
|
]);
|
|
} |