I am attempting to implement file storage in an S3-compatible blob store when a user selects a photo in an iOS mobile app. I am using the Just library on the mobile side for an authenticated POST request, and koa libraries on top of a nodejs server on the backend. Everything appears to be working perfectly when I watch the logs in xcode and on our server (ie: the photo's name, path, and type are exposed/provided in the POST request), but I receive an error in my server logs that the file/path does not exist when the S3 function is attempting to execute. I know this is a layered issue but any help greatly appreciated.
Swift Code (seems to work perfectly):
Backend Code:
I have tried countless iterations of the code above. A primary issue is that ctx.request.files will return data when the POST executes, but in writing the code an error of the object being "potentially undefined" pops up when I try to set variables to properties such as ctx.request.files.testimage, .file, etc...
A version the came close to success:
Using multer's Single function worked well for accessing properties of ctx.file, but the path was not exposed as one of the properties (hence my moving on to use multer's Any function), so I tried to used ctx.file.buffer with the S3 function to no avail.
S3 Upload:
Swift Code (seems to work perfectly):
Code Block func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { if let editedImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage { selectPhotoImageView.image = editedImage let imgUrl = info[UIImagePickerController.InfoKey.imageURL] as! URL let imgName = imgUrl.lastPathComponent let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first let localPath = documentDirectory?.appending(imgName) let data = editedImage.jpegData(compressionQuality: 0.5)! as NSData data.write(toFile: localPath!, atomically: true) let photoURL = URL.init(fileURLWithPath: localPath!) let partFilename = photoURL.lastPathComponent if let URLContent = try? Data(contentsOf: photoURL) { let partContent = URLContent } let resp = SessionManager.shared.just()?.post( "https://app.myta.io/api/1/file-upload", data: ["path": photoURL, "filename": "testimage"], files: ["file": .url(photoURL, "image/jpeg")] ) if (resp != nil && resp!.ok) { print("Made successful request") } else { print("Request failed :(((") }
Backend Code:
Code Block import { authenticateJWT, checkAuthenticated } from 'server/auth'; import { Context, DefaultState } from 'koa'; import Koa from 'koa'; import mount from 'koa-mount'; import Router from 'koa-router'; import { addUploadedFile } from 'server/external/file_upload'; import koaBody from 'koa-body'; const multer = require('@koa/multer'); const upload = multer(); const fs = require('fs'); /** * setup router for authenticated API routes */ const apiRouter = new Router<DefaultState, Context>(); apiRouter.use(authenticateJWT); apiRouter.get( '/authenticated', async (ctx) => (ctx.body = 'Hi, authenticated user!'), ); /** * upload file with koa/multer */ apiRouter.post('/file-upload', upload.any('file'), async (ctx) => { console.log('ctx.request.body', ctx.request.body); console.log('ctx.request.files', ctx.request.files); /* shows up in logs, but I can't use properties from it */ const type = ctx.request.type; const user = ctx.state.user; try { const fileName = ctx.request.body.filename; const fileContents = ctx.request.body.path; /* S3 function thinks this path does not exist, even though I see it in the server logs */ const fileType = 'image/jpeg'; try { const fileId = addUploadedFile(fileName, fileContents, fileType, user); if (fileId) { ctx.status = 200; ctx.body = { status: 'success', }; } else ctx.status = 400; } catch { ctx.status = 400; } } catch(err) { console.log(`error ${err.message}`) } }); const app = new Koa(); app.use(apiRouter.routes()).use(apiRouter.allowedMethods()); export const apiApp = mount('/api/1', app);
I have tried countless iterations of the code above. A primary issue is that ctx.request.files will return data when the POST executes, but in writing the code an error of the object being "potentially undefined" pops up when I try to set variables to properties such as ctx.request.files.testimage, .file, etc...
A version the came close to success:
Code Block apiRouter.post('/file-upload', upload.single('file'), async (ctx) => { console.log('ctx.file', ctx.file); const type = ctx.request.type; const user = ctx.state.user; try { const fileName = ctx.file.fieldname; const fileContents = fs.createReadStream(new Uint8Array(ctx.file.buffer)); /* S3 function will not accept this buffer, even when not explicitly converted to Uint8Array */ const fileType = ctx.file.mimetype; try { const fileId = addUploadedFile(fileName, fileContents, fileType, user); if (fileId) { ctx.status = 200; ctx.body = { status: 'success', }; } else ctx.status = 400; } catch { ctx.status = 400; } } catch(err) { console.log(`error ${err.message}`) } });
Using multer's Single function worked well for accessing properties of ctx.file, but the path was not exposed as one of the properties (hence my moving on to use multer's Any function), so I tried to used ctx.file.buffer with the S3 function to no avail.
S3 Upload:
Code Block import AWS from 'aws-sdk'; import { db, User } from 'server/db'; const fs = require('fs'); const spacesEndpoint = new AWS.Endpoint('nyc3.digitaloceanspaces.com'); const s3 = new AWS.S3({ endpoint: spacesEndpoint, accessKeyId: process.env.FILE_UPLOAD_SPACES_KEY, secretAccessKey: process.env.FILE_UPLOAD_SPACES_SECRET, }); const spacesFileUploadBucket = 'myta-uploads'; export async function addUploadedFile( fileName: string, fileContents: string, fileType: string, user: User, ): Promise<string | null | undefined> { const fileNameRegex = /^[a-zA-Z0-9-._]{1,30}$/; if (!fileName.match(fileNameRegex)) { throw new Error('Invalid file name'); } const body = fs.createReadStream(fileContents); const params = { Bucket: spacesFileUploadBucket, Key: fileName, Body: body, Type: fileType, ACL: 'private', }; if (!user) throw new Error('No user given'); let fileId; await s3.putObject(params, async function (err) { if (err) throw new Error('Could not add file to storage'); fileId = await addFileUploadedToDb(fileName, user); }); return fileId; } async function addFileUploadedToDb(fileName: string, user: User) { const file = await db.fileUpload.create({ data: { fileName: fileName, uploader: { connect: { id: user.id } }, }, }); return file.id; }