I would like to use the AWS iOS SDK to upload an image directly from a public url to a S3 bucket.
My goal is to avoid downloading then uploading the image, which would be obviously slower.
I tried to naively pass the URL to AWSS3TransferUtility.uploadFile like so :
import AWSCore
import AWSS3
// ...
AWSS3TransferUtility.register(
with: AWSServiceManager.default().defaultServiceConfiguration!,
transferUtilityConfiguration: AWSS3TransferUtilityConfiguration(),
forKey: "foo"
)
let utility = AWSS3TransferUtility.s3TransferUtility(forKey: "foo")!
let imageURL = URL(string: "https://via.placeholder.com/150/")!
utility
.uploadFile(
imageURL,
bucket: "<bucket name>",
key: "bar.jpeg",
contentType: "image/jpg",
expression: nil,
completionHandler: nil)
.continueWith {
if let error = $0.error {
print("Error: \(error.localizedDescription)")
}
return nil
}
But it seems to accept only url of local files and return the error
The operation couldn’t be completed. (com.amazonaws.AWSS3TransferUtilityErrorDomain error 4.)
Any idea if this is possible and how ?
Based on #jarmod idead I was able to do it using a Lambda (simplified here) :
const https = require('https');
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
function loadImage(url) {
return new Promise((resolve, reject) => {
https.get(url, (resp) => {
if(resp.statusCode > 200) {
reject('The image could not be loaded (HTTP ' + resp.statusCode + ')');
}
resp.setEncoding('binary');
let chunks = [];
resp.on('data', (chunk) => {
chunks.push(Buffer.from(chunk, 'binary'));
});
resp.on('end', () => {
let binary = Buffer.concat(chunks);
resolve(binary);
});
}).on("error", (err) => {
console.log("Error: " + err.message);
reject(err);
});
});
}
exports.handler = async (event, context, callback) => {
try {
const url = ...;
const bucket = ...;
const key = ...;
const buffer = await loadImage(url);
const destparams = {
Bucket: bucket,
Key: key,
Body: buffer,
ContentType: "image"
};
await s3.putObject(destparams).promise();
return "OK";
} catch(error) {
return error;
}
};
Calling a Lambda using AWSLambda is quite simple :
let lambda = AWSLambdaInvocationRequest()!
lambda.functionName = // Lambda name
lambda.invocationType = .requestResponse
let parameters: [String: Any] = [:] // Parameters to pass to the lambda
let payload = try! JSONSerialization.data(withJSONObject: parameters, options: [])
lambda.payload = payload
AWSLambda.default().invoke(lambda) { response, error in
//...
}
Related
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 (which seems to work perfectly):
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:
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); // displays properties in the server logs, but I can not set properties to ctx.request.files.filename, for example
const type = ctx.request.type;
const user = ctx.state.user;
try {
const fileName = ctx.request.body.filename;
const fileContents = ctx.request.body.path; // The S3 function below tells me this path doesn't exist, even though I see it in the logs and it is accurate
const fileType = 'image/jpeg';
console.log(`name: ${fileName}`);
console.log(`contents: ${fileContents}`);
console.log(`type: ${fileType}`);
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:
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;
console.log('user: ', JSON.stringify(user, null, 2));
try {
const fileName = ctx.file.fieldname;
const fileContents = fs.createReadStream(new Uint8Array(ctx.file.buffer)); // The S3 function does not like this buffer, neither as-is or when explicitly converted to Uint8Array as shown here
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 (as seen above), 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:
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;
}
I just got my first Lambda function written, but it does not work at this point.
I tried a number of variations in the code; partly following what I could think of and partly following what I could come across on the net; but all failed.
I want the Lambda function to listUsers in a UserPool and get an email for a given sub passed as parameter.
Here is the Swift function making the call to the Lambda function:
func getLambdaInfo() {
let lambdaInvoker = AWSLambdaInvoker.default(),
jsonObject:[String: Any] = ["sub" : "MY-USER-SUB"]
lambdaInvoker.invokeFunction("myLambdaFunc", jsonObject: jsonObject)
.continueWith(block: {
(task:AWSTask<AnyObject>) -> Any? in
if( task.error != nil) {
print("Error: \(task.error!)")
return nil
}
print("\(#function) ---- WE ARE HERE!!!!")
// Handle response in task.result:
if let JSONDictionary = task.result as? NSDictionary {
print("Result: \(JSONDictionary)")
}
return nil
})
}
Here is the Lambda function:
var AWS = require('aws-sdk/dist/aws-sdk-react-native');
exports.handler = async (event,context) => {
var params = {
UserPoolId: 'MY-POOL-ID',
AttributesToGet: ['email'],
Limit: '2'
};
var cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider();
cognitoidentityserviceprovider.listUsers(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
// How can I get this data sent in the response is probably the issue ??
});
const response = {
inBound: event.sub,
statusCode: 200,
body: JSON.stringify('Hello from Lambda!')
};
return response;
}
Here is what can be seen in the Xcode debugging console:
getLambdaInfo() ---- WE ARE HERE!!!!
Result: {
body = "\"Hello from Lambda!\"";
inBound = "MY-USER-SUB";
statusCode = 200;
}
I hope someone with more AWSLambda than me will be able to give me some hints concerning the changes I need to make in my code to get the result (email address) I want (into my Swift getLambdaInfo()).
You need to move your return statement in the callback of listUsers:
cognitoidentityserviceprovider.listUsers(params, function(err, data) {
if (err) {
console.log(err, err.stack); // an error occurred
// return a 500 error ?
}
else {
console.log(data);
const response = {
inBound: event.sub,
statusCode: 200,
body: JSON.stringify(data)
}
return response;
}
});
Since you're using the async pattern you can also do:
try {
const data = await cognitoidentityserviceprovider.listUsers(params).promise() // note the await and .promise() here
const response = {
inBound: event.sub,
statusCode: 200,
body: JSON.stringify(data)
}
return response;
} catch (err) {
// do something with err
}
Otherwise your Lambda function returns before your callback gets executed (async nature of JavaScript).
I am creating excel file from user data but unfortunately its not generating file and even don't know what is the error so I can at-least try to solve that error.
Specially this issue occur in iOS platform only.
Please find below code to generate excel file:
public createXSLX(): Promise<any> {
return new Promise((resolve) => {
let sheet = XLSX.utils.json_to_sheet(this.data);
let wb = {
SheetNames: ["export"],
Sheets: {
"export": sheet
}
};
let wbout = XLSX.write(wb, {
bookType: 'xlsx',
bookSST: false,
type: 'binary'
});
function s2ab(s) {
let buf = new ArrayBuffer(s.length);
let view = new Uint8Array(buf);
for (let i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
return buf;
}
let blob = new Blob([s2ab(wbout)], { type: 'application/octet-stream' });
resolve(blob);
});
}
This above function works very well in android but in iOS its not generation file from provided data.
How I call above function code :
onExportNew = function (toEmail) {
this.createXSLX().then((xclBlob) => {
let time = new Date().getTime();
let fileName: string = "roster_" + time + ".xlsx";
var fs = ''
if (this.platform.is('ios')){
fs = this.file.documentsDirectory;
}else{
fs = this.file.externalDataDirectory;
}
console.log("File Path:- ",fs)
this.file.writeFile(fs, fileName, xclBlob, true).then(() => {
let fp = fs + fileName;
let email = {
// to: 'lmahajan#cisco.com',
// cc: 'erika#mustermann.de',
// bcc: ['john#doe.com', 'jane#doe.com'],
to: toEmail,
attachments: [fp],
subject: 'Roster Excel File',
body: '<h1>PFA</h1>',
isHtml: true
};
this.emailComposer.open(email).then(() => {
this.showDone = true;
}).catch(() => {
let toast = this.toastCtrl.create({
message: 'Could not open email composer',
duration: 3000
});
toast.present();
});
}).catch(() => {
this.displayAlert('Error', 'error creating file at: ' + fs);
});
}).catch(() => {
console.log("Excel file creation error");
});
}
Guide me if missing anything in above code.
Thanks in advance!
Every time I make a call to acquireToken, it keeps launching the AAD login window and prompts me for a username/password, even though I've already authenticated successfully and consumed an access token to make API calls.
Here is my code
Step 1. Call the loadData function from controller
loadData = (): Rx.IPromise<Array<UserResult>> => {
var url = this.xxxApiUrl;
return Http.get<Array<UserResult>>(this._$http, url);
};
Step -2
export function get<TResult>(http: ng.IHttpService, url: string,
ignoreLoadingBar: boolean = false, retryCount = 0): Rx.IPromise<TResult> {
var req: any = {};
if (ignoreLoadingBar) {
req.ignoreLoadingBar = ignoreLoadingBar;
}
let resObservable = Rx.Observable.create(subscriber => {
acquireToken(url, (message, token) => {
req.headers.Authorization = `Bearer ${token}`;
http.get(url, req)
.then(res => {
subscriber.onNext(res.data);
subscriber.onCompleted();
}, (err) => { alert(JSON.stringify(err)); });
});
});
return resObservable.toPromise();
}
function acquireToken(apiUrl: string, callback) {
let innerCallback = (res) => callback('', res.accessToken);
let xConfig= JSON.parse(<any>sessionStorage.getItem('xConfig'));
window.AuthenticationContext = new
window.Microsoft.ADAL.AuthenticationContext
(xConfig.common.azure.authorityTenant);
window.AuthenticationContext.tokenCache.readItems().then(items => {
if (items.length > 0) {
let authority = items[0].authority;
window.AuthenticationContext = new
window.Microsoft.ADAL.AuthenticationContext(authority);
}
let resourceUri = getResourceUri(xConfig, apiUrl);
window.AuthenticationContext.acquireTokenSilentAsync(resourceUri,
xConfig.common.azure.clientId, xConfig.common.azure.redirectUri)
.then(innerCallback, (err) => {
window.AuthenticationContext.acquireTokenAsync(resourceUri,
xConfig.common.azure.clientId, xConfig.common.azure.redirectUri)
.then(innerCallback);
});
});
}
Looking at your code, it looks like that you are using acquireTokenSilentAsync using the common endpoint, this is not supported. Please make sure to use your tenant Id or name (like tenant.onmicrosoft.com) instead of common when using acquireTokenSilentAsync
For more information about the common endpoint please see here
This is the function I am using to upload file but is is giving me the error : Length is undefined. what I have to change in this code. where to give path of file to upload.
fileChange(event) {
let fileList: FileList = event.target.files;
if(fileList) {
let file: File = fileList[0];
let formData:FormData = new FormData();
formData.append('uploadFile', file, file.name);
let headers = new Headers();
/** No need to include Content-Type in Angular 4 */
headers.append('Content-Type', 'multipart/form-data');
headers.append('Accept', 'application/json');
let options = new RequestOptions({ headers: headers });
this.http.post(`assets/Files/info.txt`, formData, options)
.map(res => res.json())
.catch(error => Observable.throw(error))
.subscribe(
data => console.log(fileList),
error => console.log(error)
)
}
}
you need to use xhr request to transfer files
fileChange(event: EventTarget) {
let eventObj: MSInputMethodContext = <MSInputMethodContext> event;
let target: HTMLInputElement = <HTMLInputElement> eventObj.target;
let files: FileList = target.files;
if(files) {
let file: File = files[0];
this.upload(file)
}
}
public upload(filedata: File) {
let url = 'your url'
if (typeof filedata != 'undefined') {
return new Promise((resolve, reject) => {
let formData: any = new FormData();
let xhr = new XMLHttpRequest();
formData.append('icondata', filedata, filedata.name);
xhr.open('POST', url, true);
xhr.setRequestHeader('Authorization', 'JWT ' + localStorage.getItem('id_token'));
xhr.send(formData);
xhr.onreadystatechange = function () {
if (xhr.readyState == XMLHttpRequest.DONE) {
resolve(JSON.parse(xhr.responseText));
}
}
});
}
}
I understand that this is not the functionality you want to have but with no backend you can not upload files to be persistent, they should be stored somewhere. If you just wanna manipulate file names for instance, skip the express part in my answer. I personally used this code which I altered to upload multiple files.
In your Component :
import {FormArray, FormBuilder, FormControl, FormGroup} from "#angular/forms";
declare FormBuilder in the constructor:
constructor (private http: Http, private fb: FormBuilder) {}
in ngOnInit() set a variable as follows :
this.myForm = this.fb.group({chosenfiles: this.fb.array([])});
this is the code for the upload method :
// invoke the upload to server method
// TODO
// Should be in a service (injectable)
upload() {
const formData: any = new FormData();
const files: Array<File> = this.filesToUpload;
//console.log(files);
const chosenf = <FormArray> this.myForm.controls["chosenfiles"];
// iterate over the number of files
for(let i =0; i < files.length; i++){
formData.append("uploads[]", files[i], files[i]['name']);
// store file name in an array
chosenf.push(new FormControl(files[i]['name']));
}
this.http.post('http://localhost:3003/api/upload', formData)
.map(files => files.json())
.subscribe(files => console.log('upload completed, files are : ', files));
}
the method responsible for the file change :
fileChangeEvent(fileInput: any) {
this.filesToUpload = <Array<File>>fileInput.target.files;
const formData: any = new FormData();
const files: Array<File> = this.filesToUpload;
console.log(files);
const chosenf = <FormArray> this.myForm.controls["chosenfiles"];
// iterate over the number of files
for(let i =0; i < files.length; i++){
formData.append("uploads[]", files[i], files[i]['name']);
// store file name in an array
chosenf.push(new FormControl(files[i]['name']));
}
}
Template is something like this
<input id="cin" name="cin" type="file" (change)="fileChangeEvent($event)" placeholder="Upload ..." multiple/>
Notice multiple responsible for allowing multiple selections
The express API which will handle the request uses multer after an npm install
var multer = require('multer');
var path = require('path');
specify a static directory which will hold the files
// specify the folder
app.use(express.static(path.join(__dirname, 'uploads')));
As specified by multer
PS: I did not investigate multer, as soon as i got it working, i moved to another task but feel free to remove unnecessary code.
var storage = multer.diskStorage({
// destination
destination: function (req, file, cb) {
cb(null, './uploads/')
},
filename: function (req, file, cb) {
cb(null, file.originalname);
}
});
var upload = multer({ storage: storage });
And finally the endpoint
app.post("/api/upload", upload.array("uploads[]", 12), function (req, res) {
console.log('files', req.files);
res.send(req.files);
});