260 lines
8.2 KiB
JavaScript
260 lines
8.2 KiB
JavaScript
import axios from "axios";
|
|
import Constants from "expo-constants";
|
|
import * as ImagePicker from "expo-image-picker";
|
|
import { END, eventChannel } from 'redux-saga';
|
|
import { all, call, delay, fork, put, select, take, takeEvery, takeLatest } from "redux-saga/effects";
|
|
import env from "../../env";
|
|
import { axiosAuthInterceptorId } from "../../util/CleanAxios";
|
|
import { fetchArrayBufferFromUri, fetchImageFromUri, replaceAccents } from '../../util/uploadUtils';
|
|
import { selectBodyshop } from "../user/user.selectors";
|
|
import {
|
|
mediaUploadComplete,
|
|
mediaUploadFailure,
|
|
mediaUploadProgressOne,
|
|
mediaUploadStart
|
|
} 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,
|
|
});
|
|
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);
|
|
|
|
// 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(mediaUploadComplete());
|
|
|
|
} catch (error) {
|
|
console.log("Saga Error: upload start", error);
|
|
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 photoArrayBuffer = yield fetchArrayBufferFromUri(photo.uri);
|
|
|
|
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}`
|
|
|
|
if (bodyshop.uselocalmediaserver) {
|
|
yield call(uploadToLocalMediaServer, photo, photoBlob, photoArrayBuffer, key, bodyshop, jobid);
|
|
} else {
|
|
yield call(uploadToImageProxy, photo, photoBlob, photoArrayBuffer, key, bodyshop, jobid);
|
|
}
|
|
|
|
// yield put(mediaUploadSuccess({ photoId, photo }));
|
|
|
|
} catch (error) {
|
|
console.log(`Upload failed for photo ${photo.assetId}:`, error);
|
|
yield put(mediaUploadFailure({ ...photo, status: "error", error: error.message }));
|
|
}
|
|
}
|
|
|
|
function* uploadToLocalMediaServer(photo, key) {
|
|
try {
|
|
// yield put(mediaUploadProgress({ photoId, status: 'uploading', progress: 25 }));
|
|
|
|
// const formData = new FormData();
|
|
// formData.append('file', {
|
|
// uri: photo.uri,
|
|
// type: photo.type || 'image/jpeg',
|
|
// name: photo.fileName || `photo_${Date.now()}.jpg`,
|
|
// });
|
|
|
|
// yield put(mediaUploadProgress({ photoId, status: 'uploading', progress: 50 }));
|
|
|
|
// const response = yield call(fetch, 'YOUR_LOCAL_MEDIA_SERVER_ENDPOINT', {
|
|
// method: 'POST',
|
|
// body: formData,
|
|
// headers: {
|
|
// 'Content-Type': 'multipart/form-data',
|
|
// },
|
|
// });
|
|
|
|
// yield put(mediaUploadProgress({ photoId, status: 'uploading', progress: 75 }));
|
|
|
|
// if (!response.ok) {
|
|
// throw new Error(`Upload failed: ${response.status}`);
|
|
// }
|
|
|
|
// const result = yield call([response, 'json']);
|
|
// yield put(mediaUploadProgress({ photoId, status: 'completed', progress: 100 }));
|
|
|
|
// return result;
|
|
|
|
} catch (error) {
|
|
throw new Error(`Local media server upload failed: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
function* uploadToImageProxy(photo, photoBlob, photoArrayBuffer, 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];
|
|
|
|
// Create an eventChannel to bridge imperative upload progress events into the saga world
|
|
const uploadChannel = yield call(createAxiosUploadChannel, {
|
|
url: preSignedUploadUrlToS3,
|
|
data: photoArrayBuffer,
|
|
method: 'put',
|
|
headers: {
|
|
'Content-Type': photoBlob.type,
|
|
'Content-Length': photoBlob.size,
|
|
},
|
|
});
|
|
|
|
try {
|
|
while (true) {
|
|
const { progress, error, success } = yield take(uploadChannel);
|
|
if (progress != null) {
|
|
yield put(mediaUploadProgressOne({ ...photo, progress }));
|
|
} else if (error) {
|
|
throw error;
|
|
} else if (success) {
|
|
yield put(mediaUploadProgressOne({ ...photo, progress: 100, status: 'completed' }));
|
|
break;
|
|
}
|
|
}
|
|
} finally {
|
|
uploadChannel.close();
|
|
}
|
|
|
|
} catch (error) {
|
|
console.log("Error uploading to image proxy", JSON.stringify(error));
|
|
throw new Error(`Image proxy upload failed: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Helper: turn axios upload progress into an eventChannel so we can yield put from saga
|
|
function createAxiosUploadChannel({ url, data, method = 'put', headers }) {
|
|
return eventChannel(emitter => {
|
|
const controller = new AbortController();
|
|
cleanAxios({
|
|
method,
|
|
url,
|
|
data,
|
|
headers,
|
|
signal: controller.signal,
|
|
// NOTE: cannot yield here, so we emit events and handle them in saga loop
|
|
onUploadProgress: (e) => {
|
|
console.log("Upload progress event:", e);
|
|
if (e && e.total) {
|
|
emitter({ progress: e.loaded / e.total });
|
|
}
|
|
}
|
|
}).then(() => {
|
|
emitter({ success: true });
|
|
emitter(END);
|
|
}).catch(err => {
|
|
emitter({ error: err });
|
|
emitter(END);
|
|
});
|
|
|
|
// Unsubscribe / cancellation logic
|
|
return () => {
|
|
controller.abort()
|
|
};
|
|
});
|
|
}
|
|
|
|
// 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)
|
|
]);
|
|
} |