Uploading file with corruption.

This commit is contained in:
Patrick Fic
2025-10-10 11:34:11 -07:00
parent 620b5135d1
commit 9e02d5a9b7
7 changed files with 412 additions and 20 deletions

View File

@@ -8,4 +8,14 @@ export const openImagePicker = (jobid) => ({
export const mediaUploadStart = (imagePickerResult) => ({
type: PhotosActionTypes.MEDIA_UPLOAD_START,
payload: imagePickerResult,
})
})
export const mediaUploadProgressOne = (progressUpdate) => ({
type: PhotosActionTypes.MEDIA_UPLOAD_PROGRESS_UPDATE_ONE,
payload: progressUpdate,
});
export const mediaUploadFailure = (error) => ({
type: PhotosActionTypes.MEDIA_UPLOAD_FAILURE,
payload: error,
});

View File

@@ -4,6 +4,8 @@ const INITIAL_STATE = {
photos: [],
uploadInProgress: false,
uploadError: null,
jobid: null,
progress: {}
};
const photosReducer = (state = INITIAL_STATE, action) => {
@@ -11,9 +13,22 @@ const photosReducer = (state = INITIAL_STATE, action) => {
case PhotosActionTypes.MEDIA_UPLOAD_START:
return {
...state,
photos: action.payload,
photos: action.payload.photos,
jobid: action.payload.jobid,
uploadInProgress: true,
uploadError: null,
progress: {}
};
case PhotosActionTypes.MEDIA_UPLOAD_FAILURE:
return {
...state,
uploadInProgress: false,
uploadError: action.payload,
};
case PhotosActionTypes.MEDIA_UPLOAD_PROGRESS_UPDATE_ONE:
return {
...state,
progress: { ...state.progress, [action.payload.assetId]: { ...state.progress[action.payload.assetId], ...action.payload } }
};
default:

View File

@@ -1,14 +1,31 @@
import axios from "axios";
import Constants from "expo-constants";
import * as ImagePicker from "expo-image-picker";
import { all, call, put, select, takeLatest } from "redux-saga/effects";
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 { fetchImageFromUri, replaceAccents } from '../../util/uploadUtils';
import { selectBodyshop } from "../user/user.selectors";
import { mediaUploadStart } from "./photos.actions";
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(jobid) {
export function* openImagePickerAction({ payload: jobid }) {
try {
if (Constants.platform.ios) {
const cameraRollStatus =
@@ -29,37 +46,214 @@ export function* openImagePickerAction(jobid) {
allowsMultipleSelection: true,
});
if (!(result.canceled)) {
yield put(mediaUploadStart(result.assets));
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(photos) {
export function* mediaUploadStartAction({ payload: { photos, jobid } }) {
try {
console.log("Got to the Photo Saga.", photos);
console.log("Starting upload for", photos.length, "photos");
console.log("upload", photos)
//get bodyshop state
const bodyshop = yield select(selectBodyshop);
if (bodyshop.uselocalmediaserver) {
//upload to LMS
} else {
//Upload to img proxy
}
// 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: open upload", 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.
console.log("*** ~ uploadSinglePhoto ~ photoBlob.name.:", photoBlob.data.name);
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, key, bodyshop, jobid);
} else {
yield call(uploadToImageProxy, photo, photoBlob, 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, 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];
console.log("*** ~ uploadToImageProxy ~ s3Key:", s3Key);
console.log("*** ~ uploadToImageProxy ~ presignedUrl:", preSignedUploadUrlToS3);
// Create an eventChannel to bridge imperative upload progress events into the saga world
const uploadChannel = yield call(createAxiosUploadChannel, {
url: preSignedUploadUrlToS3,
data: photoBlob,
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) {
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) => {
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)]);
}
yield all([
call(onOpenImagePicker),
call(onMediaUploadStart),
//call(onCancelUpload)
]);
}

View File

@@ -3,6 +3,6 @@ const PhotosActionTypes = {
MEDIA_UPLOAD_START: "MEDIA_UPLOAD_START",
MEDIUA_UPLOAD_SUCCESS: "MEDIA_UPLOAD_SUCCESS",
MEDIA_UPLOAD_FAILURE: "MEDIA_UPLOAD_FAILURE",
MEDIA_UPLOAD_PROGRESS_UPDATE: "MEDIA_UPLOAD_PROGRESS_UPDATE",
MEDIA_UPLOAD_PROGRESS_UPDATE_ONE: "MEDIA_UPLOAD_PROGRESS_UPDATE_ONE",
};
export default PhotosActionTypes;