how to upload images as a signed request with Cloudinary/Angular(5) and Ionic(3)? - ionic-framework

Cloudinary have a basic.js example which I'm trying to implement in my Ionic/Angular project.
Problem is, for some reason the Ionic version of "#cloudinary/angular-5.x" always uses the unsigned_upload feature, and I want to be able to transform it before I upload, same as the Cloudinary example.
Transformation requires signed upload not unsigned upload.
Since there are many versions out-there, and most of the examples don't work, mine is:
Ionic: 3
Angular: 5.2.11
Cloudinary:
"cloudinary": "^1.11.0",
"cloudinary-core": "^2.5.0",
"#cloudinary/angular-5.x": "^1.0.2"
basic.js
My configuration is inside the .env variable with the structure mentioned in cloudinary.config
var dotenv = require('dotenv');
dotenv.load();
var fs = require('fs');
var cloudinary = require('cloudinary').v2;
// set your env variable CLOUDINARY_URL or set the following configuration
/*cloudinary.config({
cloud_name: '',
api_key: '',
api_secret: ''
});*/
var url = "http://res.cloudinary.com/demo/image/upload/couple.jpg"
cloudinary.uploader.upload(url,{"tags":"basic_sample","width":500,"height":500,"crop":"fit","effect":"saturation:-70"} ,
function(err,image){
if (err){
console.warn(err);
return;
}
console.log("* "+image.public_id);
console.log("* "+image.url);
// Transform image
cloudinary.url(image.public_id,
{
width: 200,
height: 150,
crop: "fill",
gravity: "face",
radius: 10,
effect:"sepia",
format: "jpg"
}
));
});
I'm able with the following code to upload it unsigned
Ionic unsigned request
ngOnInit(): void {
const uploaderOptions: FileUploaderOptions = {
url: 'https://api.cloudinary.com/v1_1/' + this.cloudinary.config().cloud_name + '/upload',
autoUpload: false,
isHTML5: true,
removeAfterUpload: true,
headers: [{
name: 'X-Requested-With',
value: 'XMLHttpRequest'
}]
};
this.uploader = new FileUploader(uploaderOptions);
// Add custom tag for displaying the uploaded photo in the list
this.uploader.onBuildItemForm = (fileItem: any, form: FormData): any => {
form.append('upload_preset', this.cloudinary.config().upload_preset);
form.append('public_id', 'subfolder/' + this.UUID);
form.append('file', fileItem);
fileItem.withCredentials = false;
return { fileItem, form };
};
}
Ionic signed request
So in order to transform my images, I need to use parameter called eager
form.append('eager', 'c_crop,w_191,h_145,g_face,z_0.7');
But then I get the below error
Upload completed with status code 400
{
"message": "Eager parameter is not allowed when using unsigned upload.
Only upload_preset,callback,public_id,folder,tags,context,face_coordinates,custom_coordinates,source upload parameters are allowed.
}
When I remove the preset to "tell" it that maybe this is a signed request, I get the above error + Upload preset must be specified when using unsigned upload
So I'm not sure how I'm suppose to "tell" it - use signed request, and take my configuration from .env or CloudinaryModule.forRoot({Cloudinary}, cloudinaryConfiguration as CloudinaryConfiguration), etc ...

For signed upload, you need to create a signature. During post request, you have to attach it with form.
Signature is SHA1 hexadecimal string which is consists of timestamp(unixtime), public_id (any text) and your cloudinary API_SECRET.
Here is my workable sample
private generateSignature() {
this.public_id = `image_${Date.now()}`; // I like to make it unique.
this.unixtime = Date.now() / 1000 | 0;
return CryptoJS.SHA1(`public_id=${this.public_id}&timestamp=${this.unixtime}${this.API_SECRET}`).toString()
}
here I use CryptoJS for encription.
Append this signature with form body before send API request.
for example
initFileUploader(): void {
const self = this;
self.uploader = new FileUploader({
url: 'https://api.cloudinary.com/v1_1/your_cloud_name/upload',
allowedMimeType: ['image/png', 'image/jpg', 'image/jpeg', 'image/gif'],
maxFileSize: 524288,//512 KB
autoUpload: true,
removeAfterUpload: true,
isHTML5: true,
headers: [
{
name: 'X-Requested-With',
value: 'XMLHttpRequest'
}
]
});
self.uploader.onAfterAddingFile = (file) => {
file.withCredentials = false;
};
self.uploader.onSuccessItem = (item, response, status) => {
const resp = <any>JSON.parse(response);
if (resp) {
this.onSuccess.emit(resp);
} else {
this.onError.emit('An error occured during upload. Please retry');
}
};
self.uploader.setOptions(self._uploaderOptions);
self.uploader.onBuildItemForm = (fileItem: any, form: FormData): any => {
let signature = this.generateSignature();
form.append('timestamp', this.unixtime.toString());
form.append('public_id', this.public_id);
form.append('api_key', this.API_KEY); //your cloudinary API_KEY
form.append('signature', signature);
return { fileItem, form };
};
}
I use ng2-file-upload for uploading...

uploading images via signed method
Signed uploads require an authentication signature to be generated on your server using a function method or string method, and as such, signed upload
The current Angular SDK is outdated so we follow these steps to implement our signed upload.
Manually generate Signature via string method in Angular
To manually generate your own POST request, you need to authenticate the request with a signature based on the parameters you use in the request. The signature is a hexadecimal message digest (hash value) created with the SHA-1 or SHA-256 (Secure Hash Algorithm) cryptographic function.
You can manually generate the comparison signature instead of using the Cloudinary SDK’s api_sign_request method.
For example, if your API secret is abcd, your API key is 1234, the Unix time now is 1315060510 and you are posting a request to upload a file from ‘https://www.example.com/sample.jpg', set its Public ID as sample_image, and eagerly generate 2 images:
Parameters to sign:
timestamp: 1315060510
public_id: sample_image
eager: w_400,h_300,c_pad|w_260,h_200,c_crop
Serialized sorted parameters in a single string:
eager=w_400,h_300,c_pad|w_260,h_200,c_crop&public_id=sample_image&timestamp=1315060510
String including the API secret that is used to create the SHA-1 signature:
eager=w_400,h_300,c_pad|w_260,h_200,c_crop&public_id=sample_image&timestamp=1315060510abcd
Generate Signature in Angular
Using a native js function for hashing messages with the SHA-1 algorithm
First Install sha1
npm install sha1
Then import the package into the app
import sha1 from ‘sha1’;
Generate UUID for Public ID
Another thing we did so each upload has a unique ID was to using UUID package to generate a unique Public ID for each upload
npm install uuid
import * as uuid from ‘uuid’;
on NgInit we generate the UUID using
this.uuidValue = `${uuid.v4().toLowerCase()}`;
we the use method sha1(string) Returns the SHA-1 hash of the given message.
The result is a SHA-1 hexadecimal result:
b4ad47fb4e25c7bf5f92a20089f9db59bc302313
signuploadform() {
const timestamp = Math.round(new Date().getTime() / 1000);
const apiSecret = this.environmentService.getValue('CLOUDINARY_API_SECRET');
const api_key = this.environmentService.getValue('CLOUDINARY_API_KEY');
const signature = sha1(
'eager=c_pad,h_300,w_400|c_crop,h_200,w_260&folder=identification/NGA&public_id=' +
this.uuidValue +
'&timestamp=' +
timestamp +
apiSecret
);
return {timestamp, signature, api_key};
}
Post the Upload
Now that the signature has been generated we then post using the parameter as shown in the code below
folder
public_id
file
api_key
timestamp
signature
HTML
<input hidden (change)=”onFileChange($event)” #fileInput accept=”image/*” type=”file” id=”file”>
TS
onFileChange(event: any) {
this.uploadFile(event.target.files[0]);
}
uploadFile(file: File) {
const signData = this.signuploadform();
const formData = new FormData();
formData.append('eager', 'c_pad,h_300,w_400|c_crop,h_200,w_260');
formData.append('folder', 'identification/NGA');
formData.append('public_id', this.uuidValue);
formData.append('file', file);
formData.append('api_key', signData.api_key);
formData.append('timestamp', signData.timestamp.toString());
formData.append('signature', signData.signature);
const url =
'https://api.cloudinary.com/v1_1/' +
this.environmentService.getValue('CLOUDINARY_CLOUD_NAME') +
'/auto/upload';
this.isLoading = true;
this.http
.post(url, formData)
.pipe(map((x: any) => x.secure_url as string))
.subscribe({
next: res => {
this.identification = res;
this.uploadTitle = 'ID Uploaded';
this.uploadStatus = true;
from(
Swal.fire({
icon: 'success',
title: 'Successfully uploaded',
showConfirmButton: true,
})
);
},
error: error => {
this.isLoading = false;
from(
Swal.fire({
icon: 'error',
title: 'Please check your image again',
showConfirmButton: true,
})
);
},
complete: () => {
this.isLoading = false;
},
});
}

Related

Firebase hosting file upload via REST with Apps Script

I want to upload a file to Firebase hosting file upload via REST with Apps Script. Been trying to find a solution for days to no avail :( would highly appreciate any recommendations.
I'm following the official documentation here:
https://firebase.google.com/docs/reference/hosting/rest/v1beta1/sites.versions/populateFiles
And I can successfully get the upload URL using this code:
function getUploadURL() {
const YOUR_PROJECT_ID = 'sites/url-shortener-e42ec/versions/dd393a80797d713d';
let postUrl = 'https://firebasehosting.googleapis.com/v1beta1/YOUR_PROJECT_ID:populateFiles';
postUrl = postUrl.replace('YOUR_PROJECT_ID', YOUR_PROJECT_ID);
const options = {
method: 'post',
headers: {
Authorization: `Bearer ${ScriptApp.getOAuthToken()}`,
},
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(postUrl, options);
Logger.log(response);
}
which returns the following:
{
"uploadUrl": "https://upload-firebasehosting.googleapis.com/upload/sites/url-shortener-e42ec/versions/dd393a80797d713d/files"
}
And this is where I get kinda lost because I'm not quite sure on what to do next. The documentation says:
map (key: string, value: string)
A set of file paths to the hashes corresponding to assets that should be added to the version.
A file path to an empty hash will remove the path from the version.
Calculate a hash by Gzipping the file then taking the SHA256 hash of the newly compressed file.
But if I add a payload with a file hash to the call like so:
{
"files": {
"/teste": "3f0749957a1c4d91ed18b8e9df122709974e4e9c94c57f9245794c21dd76d4bd"
}
}
...then I get the error:
{
"error": {
"code": 400,
"message": "Precondition check failed.",
"status": "FAILED_PRECONDITION"
}
}
PART 2 :
The next issue I found is, now that I have the upload URL, I will need to actually upload the file, and according to their documentation I should:
Perform a multipart POST of the Gzipped file contents to the URL using a forward slash and the hash of the file appended to the end.
which I tried with the following apps script code:
function convert(hash) {
return hash.map(byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
}
function postFile() {
var files = DriveApp.getFilesByName('abc.txt');
let gzip;
let hash;
if (files.hasNext()) {
var file = files.next();
gzip = Utilities.gzip(file.getBlob());
hash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, gzip.getBytes());
}
let postUrl = 'https://upload-firebasehosting.googleapis.com/upload/sites/url-shortener-e42ec/versions/dd393a80797d713d/files/' + convert(hash);
/*
var textBlob = Utilities.newBlob("abc");
const gzip = Utilities.gzip(textBlob);
const hash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, gzipFile.getBytes());
*/
const data = {
"files": {
"/test.txt": convert(hash)
}
};
const options = {
method: 'post',
headers: {
Authorization: `Bearer ${ScriptApp.getOAuthToken()}`,
accept: 'application/json',
contentType: 'application/json'
},
muteHttpExceptions: true,
payload: JSON.stringify(data)
};
const response = UrlFetchApp.fetch(postUrl, options);
Logger.log(response);
}
... and get the following error:
Couldn't process request (status=412): File url-shortener-e42ec/dd393a80797d713d/0b3b82379e00a1994a46452e8cfd8b2c43ee8599f169a9ee4176253f1a8de469 can't be uploaded.
Appreciate all the help I can get. Thanks in advance!

Image returned from REST API always displays broken

I am building a content management system for an art portfolio app, with React. The client will POST to the API which uses Mongoose to insert into a MongoDB. The API then queries the DB for the newly inserted image, and returns it to the client.
Here's my code to connect to MongoDB using Mongoose:
mongoose.connect('mongodb://localhost/test').then(() =>
console.log('connected to db')).catch(err => console.log(err))
mongoose.Promise = global.Promise
const db = mongoose.connection
db.on('error', console.error.bind(console, 'MongoDB connection error:'))
const Schema = mongoose.Schema;
const ImgSchema = new Schema({
img: { data: Buffer, contentType: String }
})
const Img = mongoose.model('Img', ImgSchema)
I am using multer and fs to handle the image file. My POST endpoint looks like this:
router.post('/', upload.single('image'), (req, res) => {
if (!req.file) {
res.send('no file')
} else {
const imgItem = new Img()
imgItem.img.data = fs.readFileSync(req.file.path)
imgItem.contentType = 'image/png'
imgItem
.save()
.then(data =>
Img.findById(data, (err, findImg) => {
console.log(findImg.img)
fs.writeFileSync('api/uploads/image.png', findImg.img.data)
res.sendFile(__dirname + '/uploads/image.png')
}))
}
})
I can see in the file structure that writeFileSync is writing the image to the disk. res.sendFile grabs it and sends it down to the client.
Client side code looks like this:
handleSubmit = e => {
e.preventDefault()
const img = new FormData()
img.append('image', this.state.file, this.state.file.name)
axios
.post('http://localhost:8000/api/gallery', img, {
onUploadProgress: progressEvent => {
console.log(progressEvent.loaded / progressEvent.total)
}
})
.then(res => {
console.log('responsed')
console.log(res)
const returnedFile = new File([res.data], 'image.png', { type: 'image/png' })
const reader = new FileReader()
reader.onloadend = () => {
this.setState({ returnedFile, returned: reader.result })
}
reader.readAsDataURL(returnedFile)
})
.catch(err => console.log(err))
}
This does successfully place both the returned file and the img data url on state. However, in my application, the image always displays broken.
Here's some screenshots:
How to fix this?
Avoid sending back base64 encoded images (multiple images + large files + large encoded strings = very slow performance). I'd highly recommend creating a microservice that only handles image uploads and any other image related get/post/put/delete requests. Separate it from your main application.
For example:
I use multer to create an image buffer
Then use sharp or fs to save the image (depending upon file type)
Then I send the filepath to my controller to be saved to my DB
Then, the front-end does a GET request when it tries to access: http://localhost:4000/uploads/timestamp-randomstring-originalname.fileext
In simple terms, my microservice acts like a CDN solely for images.
For example, a user sends a post request to http://localhost:4000/api/avatar/create with some FormData:
It first passes through some Express middlewares:
libs/middlewares.js
...
app.use(cors({credentials: true, origin: "http://localhost:3000" })) // allows receiving of cookies from front-end
app.use(morgan(`tiny`)); // logging framework
app.use(multer({
limits: {
fileSize: 10240000,
files: 1,
fields: 1
},
fileFilter: (req, file, next) => {
if (!/\.(jpe?g|png|gif|bmp)$/i.test(file.originalname)) {
req.err = `That file extension is not accepted!`
next(null, false)
}
next(null, true);
}
}).single(`file`))
app.use(bodyParser.json()); // parses header requests (req.body)
app.use(bodyParser.urlencoded({ limit: `10mb`, extended: true })); // allows objects and arrays to be URL-encoded
...etc
Then, hits the avatars route:
routes/avatars.js
app.post(`/api/avatar/create`, requireAuth, saveImage, create);
It then passes through some user authentication, then goes through my saveImage middleware:
services/saveImage.js
const createRandomString = require('../shared/helpers');
const fs = require("fs");
const sharp = require("sharp");
const randomString = createRandomString();
if (req.err || !req.file) {
return res.status(500).json({ err: req.err || `Unable to locate the requested file to be saved` })
next();
}
const filename = `${Date.now()}-${randomString}-${req.file.originalname}`;
const filepath = `uploads/${filename}`;
const setFilePath = () => { req.file.path = filepath; return next();}
(/\.(gif|bmp)$/i.test(req.file.originalname))
? fs.writeFile(filepath, req.file.buffer, (err) => {
if (err) {
return res.status(500).json({ err: `There was a problem saving the image.`});
next();
}
setFilePath();
})
: sharp(req.file.buffer).resize(256, 256).max().withoutEnlargement().toFile(filepath).then(() => setFilePath())
If the file is saved, it then sends a req.file.path to my create controller. This gets saved to my DB as a file path and as an image path (the avatarFilePath or /uploads/imagefile.ext is saved for removal purposes and the avatarURL or [http://localhost:4000]/uploads/imagefile.ext is saved and used for the front-end GET request):
controllers/avatars.js (I'm using Postgres, but you can substitute for Mongo)
create: async (req, res, done) => {
try {
const avatarurl = `${apiURL}/${req.file.path}`;
await db.result("INSERT INTO avatars(userid, avatarURL, avatarFilePath) VALUES ($1, $2, $3)", [req.session.id, avatarurl, req.file.path]);
res.status(201).json({ avatarurl });
} catch (err) { return res.status(500).json({ err: err.toString() }); done();
}
Then when the front-end tries to access the uploads folder via <img src={avatarURL} alt="image" /> or <img src="[http://localhost:4000]/uploads/imagefile.ext" alt="image" />, it gets served up by the microservice:
libs/server.js
const express = require("express");
const path = app.get("path");
const PORT = 4000;
//============================================================//
// EXPRESS SERVE AVATAR IMAGES
//============================================================//
app.use(`/uploads`, express.static(`uploads`));
//============================================================//
/* CREATE EXPRESS SERVER */
//============================================================//
app.listen(PORT);
What it looks when logging requests:
19:17:54 INSERT INTO avatars(userid, avatarURL, avatarFilePath) VALUES ('08861626-b6d0-11e8-9047-672b670fe126', 'http://localhost:4000/uploads/1536891474536-k9c7OdimjEWYXbjTIs9J4S3lh2ldrzV8-android.png', 'uploads/1536891474536-k9c7OdimjEWYXbjTIs9J4S3lh2ldrzV8-android.png')
POST /api/avatar/create 201 109 - 61.614 ms
GET /uploads/1536891474536-k9c7OdimjEWYXbjTIs9J4S3lh2ldrzV8-android.png 200 3027 - 3.877 ms
What the user sees upon successful GET request:

How to download files using axios

I am using axios for basic http requests like GET and POST, and it works well. Now I need to be able to download Excel files too. Is this possible with axios? If so does anyone have some sample code? If not, what else can I use in a React application to do the same?
Download the file with Axios as a responseType: 'blob'
Create a file link using the blob in the response from Axios/Server
Create <a> HTML element with a the href linked to the file link created in step 2 & click the link
Clean up the dynamically created file link and HTML element
axios({
url: 'http://api.dev/file-download', //your url
method: 'GET',
responseType: 'blob', // important
}).then((response) => {
// create file link in browser's memory
const href = URL.createObjectURL(response.data);
// create "a" HTML element with href to file & click
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', 'file.pdf'); //or any other extension
document.body.appendChild(link);
link.click();
// clean up "a" element & remove ObjectURL
document.body.removeChild(link);
URL.revokeObjectURL(href);
});
Check out the quirks at https://gist.github.com/javilobo8/097c30a233786be52070986d8cdb1743
Full credits to: https://gist.github.com/javilobo8
More documentation for URL.createObjectURL is available on MDN. It's critical to release the object with URL.revokeObjectURL to prevent a memory leak. In the function above, since we've already downloaded the file, we can immediately revoke the object.
Each time you call createObjectURL(), a new object URL is created, even if you've already created one for the same object. Each of these must be released by calling URL.revokeObjectURL() when you no longer need them.
Browsers will release object URLs automatically when the document is unloaded; however, for optimal performance and memory usage, if there are safe times when you can explicitly unload them, you should do so.
When response comes with a downloadable file, response headers will be something like
Content-Disposition: "attachment;filename=report.xls"
Content-Type: "application/octet-stream" // or Content-type: "application/vnd.ms-excel"
What you can do is create a separate component, which will contain a hidden iframe.
import * as React from 'react';
var MyIframe = React.createClass({
render: function() {
return (
<div style={{display: 'none'}}>
<iframe src={this.props.iframeSrc} />
</div>
);
}
});
Now, you can pass the url of the downloadable file as prop to this component, So when this component will receive prop, it will re-render and file will be downloaded.
Edit: You can also use js-file-download module. Link to Github repo
const FileDownload = require('js-file-download');
Axios({
url: 'http://localhost/downloadFile',
method: 'GET',
responseType: 'blob', // Important
}).then((response) => {
FileDownload(response.data, 'report.csv');
});
Downloading Files (using Axios and Security)
This is actually even more complex when you want to download files using Axios and some means of security. To prevent anyone else from spending too much time in figuring this out, let me walk you through this.
You need to do 3 things:
Configure your server to permit the browser to see required HTTP headers
Implement the server-side service, and making it advertise the correct file type for the downloaded file.
Implementing an Axios handler to trigger a FileDownload dialog within the browser
These steps are mostly doable - but are complicated considerably by the browser's relation to CORS. One step at a time:
1. Configure your (HTTP) server
When employing transport security, JavaScript executing within a browser can [by design] access only 6 of the HTTP headers actually sent by the HTTP server. If we would like the server to suggest a filename for the download, we must inform the browser that it is "OK" for JavaScript to be granted access to other headers where the suggested filename would be transported.
Let us assume - for the sake of discussion - that we want the server to transmit the suggested filename within an HTTP header called X-Suggested-Filename. The HTTP server tells the browser that it is OK to expose this received custom header to the JavaScript/Axios with the following header:
Access-Control-Expose-Headers: X-Suggested-Filename
The exact way to configure your HTTP server to set this header varies from product to product.
See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers for a full explanation and detailed description of these standard headers.
2. Implement the server-side service
Your server-side service implementation must now perform 2 things:
Create the (binary) document and assign the correct ContentType to the response
Assign the custom header (X-Suggested-Filename) containing the suggested file name for the client
This is done in different ways depending on your chosen technology stack. I will sketch an example using the JavaEE 7 standard which should emit an Excel report:
#GET
#Path("/report/excel")
#Produces("application/vnd.ms-excel")
public Response getAllergyAndPreferencesReport() {
// Create the document which should be downloaded
final byte[] theDocumentData = ....
// Define a suggested filename
final String filename = ...
// Create the JAXRS response
// Don't forget to include the filename in 2 HTTP headers:
//
// a) The standard 'Content-Disposition' one, and
// b) The custom 'X-Suggested-Filename'
//
final Response.ResponseBuilder builder = Response.ok(
theDocumentData, "application/vnd.ms-excel")
.header("X-Suggested-Filename", fileName);
builder.header("Content-Disposition", "attachment; filename=" + fileName);
// All Done.
return builder.build();
}
The service now emits the binary document (an Excel report, in this case), sets the correct content type - and also sends a custom HTTP header containing the suggested filename to use when saving the document.
3. Implement an Axios handler for the Received document
There are a few pitfalls here, so let's ensure all details are correctly configured:
The service responds to #GET (i.e. HTTP GET), so the Axios call must be 'axios.get(...)'.
The document is transmitted as a stream of bytes, so you must tell Axios to treat the response as an HTML5 Blob. (I.e. responseType: 'blob').
In this case, the file-saver JavaScript library is used to pop the browser dialog open. However, you could choose another.
The skeleton Axios implementation would then be something along the lines of:
// Fetch the dynamically generated excel document from the server.
axios.get(resource, {responseType: 'blob'}).then((response) => {
// Log somewhat to show that the browser actually exposes the custom HTTP header
const fileNameHeader = "x-suggested-filename";
const suggestedFileName = response.headers[fileNameHeader];
const effectiveFileName = (suggestedFileName === undefined
? "allergierOchPreferenser.xls"
: suggestedFileName);
console.log(`Received header [${fileNameHeader}]: ${suggestedFileName}, effective fileName: ${effectiveFileName}`);
// Let the user save the file.
FileSaver.saveAs(response.data, effectiveFileName);
}).catch((response) => {
console.error("Could not Download the Excel report from the backend.", response);
});
Axios.post solution with IE and other browsers
I've found some incredible solutions here. But they frequently don't take into account problems with IE browser. Maybe it will save some time to somebody else.
axios.post("/yourUrl",
data,
{ responseType: 'blob' }
).then(function (response) {
let fileName = response.headers["content-disposition"].split("filename=")[1];
if (window.navigator && window.navigator.msSaveOrOpenBlob) { // IE variant
window.navigator.msSaveOrOpenBlob(new Blob([response.data],
{ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }
),
fileName
);
} else {
const url = window.URL.createObjectURL(new Blob([response.data],
{ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download',
response.headers["content-disposition"].split("filename=")[1]);
document.body.appendChild(link);
link.click();
}
}
);
example above is for excel files, but with little changes can be applied to any format.
And on server I've done this to send an excel file.
response.contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=exceptions.xlsx")
The function to make the API call with axios:
function getFileToDownload (apiUrl) {
return axios.get(apiUrl, {
responseType: 'arraybuffer',
headers: {
'Content-Type': 'application/json'
}
})
}
Call the function and then download the excel file you get:
getFileToDownload('putApiUrlHere')
.then (response => {
const type = response.headers['content-type']
const blob = new Blob([response.data], { type: type, encoding: 'UTF-8' })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = 'file.xlsx'
link.click()
})
It's very simple javascript code to trigger a download for the user:
window.open("<insert URL here>")
You don't want/need axios for this operation; it should be standard to just let the browser do it's thing.
Note: If you need authorisation for the download then this might not work. I'm pretty sure you can use cookies to authorise a request like this, provided it's within the same domain, but regardless, this might not work immediately in such a case.
As for whether it's possible... not with the in-built file downloading mechanism, no.
axios.get(
'/app/export'
).then(response => {
const url = window.URL.createObjectURL(new Blob([response]));
const link = document.createElement('a');
link.href = url;
const fileName = `${+ new Date()}.csv`// whatever your file name .
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
link.remove();// you need to remove that elelment which is created before.
})
The trick is to make an invisible anchor tag in the render() and add a React ref allowing to trigger a click once we have the axios response:
class Example extends Component {
state = {
ref: React.createRef()
}
exportCSV = () => {
axios.get(
'/app/export'
).then(response => {
let blob = new Blob([response.data], {type: 'application/octet-stream'})
let ref = this.state.ref
ref.current.href = URL.createObjectURL(blob)
ref.current.download = 'data.csv'
ref.current.click()
})
}
render(){
return(
<div>
<a style={{display: 'none'}} href='empty' ref={this.state.ref}>ref</a>
<button onClick={this.exportCSV}>Export CSV</button>
</div>
)
}
}
Here is the documentation: https://reactjs.org/docs/refs-and-the-dom.html. You can find a similar idea here: https://thewebtier.com/snippets/download-files-with-axios/.
There are a couple of critical points most of the answers are missing.
I will try to explain in much depth here.
TLDR;
If you are creating an a tag link and initiating a download through broswer request, then
Always call window.URL.revokeObjectURL(url);. Else there can be
unnecessary memory spikes.
There is NO need to append the created link to the document body using document.body.appendChild(link);, preventing the unnecessary need to remove the child later.
For Component code and a deeper analysis, read further
First is to figure out if the API endpoint from which you are trying to download the data is public or private. Do you have control over the server or not?
If the server responds with
Content-Disposition: attachment; filename=dummy.pdf
Content-Type: application/pdf
Browser will always try to download the file with the name 'dummy.pdf'
If the server responds with
Content-Disposition: inline; filename=dummy.pdf
Content-Type: application/pdf
Browser will first try to open a native file reader if available with the name 'dummy.pdf', else it will start file download.
If the server responds with neither of the above 2 headers
Browser (atleast chrome) will try to open the file if the download attribute is not set. If set, it will download the file. The name of the file will be the value of the last path param in cases where the url is not a blob.
Apart from that keep in mind to use Transfer-Encoding: chunked from server to transfer large volumes of data from the server. This will ensure the client knows when to stop reading from the current request in the absence of Content-Length header
For Private Files
import { useState, useEffect } from "react";
import axios from "axios";
export default function DownloadPrivateFile(props) {
const [download, setDownload] = useState(false);
useEffect(() => {
async function downloadApi() {
try {
// It doesn't matter whether this api responds with the Content-Disposition header or not
const response = await axios.get(
"http://localhost:9000/api/v1/service/email/attachment/1mbdoc.docx",
{
responseType: "blob", // this is important!
headers: { Authorization: "sometoken" },
}
);
const url = window.URL.createObjectURL(new Blob([response.data])); // you can mention a type if you wish
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "dummy.docx"); //this is the name with which the file will be downloaded
link.click();
// no need to append link as child to body.
setTimeout(() => window.URL.revokeObjectURL(url), 0); // this is important too, otherwise we will be unnecessarily spiking memory!
setDownload(false);
} catch (e) {} //error handling }
}
if (download) {
downloadApi();
}
}, [download]);
return <button onClick={() => setDownload(true)}>Download Private</button>;
}
For Public Files
import { useState, useEffect } from "react";
export default function DownloadPublicFile(props) {
const [download, setDownload] = useState(false);
useEffect(() => {
if (download) {
const link = document.createElement("a");
link.href =
"http://localhost:9000/api/v1/service/email/attachment/dummy.pdf";
link.setAttribute("download", "dummy.pdf");
link.click();
setDownload(false);
}
}, [download]);
return <button onClick={() => setDownload(true)}>Download Public</button>;
}
Good to know:
Always control file downloads from server.
Axios in the browser uses XHR under the hood, in which streaming of responses
is not supported.
Use onDownloadProgress method from Axios to implement progress bar.
Chunked responses from server do not ( cannot ) indicate Content-Length. Hence you need some way of knowing the response size if you are using them while building a progress bar.
<a> tag links can only make GET HTTP requests without any ability to send headers or
cookies to the server (ideal for downloading from public endpoints)
Brower request is slightly different from XHR request made in code.
Ref: Difference between AJAX request and a regular browser request
File download with custom header request. In this example, it shows how to send file download request with the bearer token. Good for downloadable content with authorization.
download(urlHere) {
axios.get(urlHere, {
headers: {
"Access-Control-Allow-Origin": "*",
Authorization: `Bearer ${sessionStorage.getItem("auth-token")}`,
}
}).then((response) => {
const temp = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = temp;
link.setAttribute('download', 'file.csv'); //or any other extension
document.body.appendChild(link);
link.click();
});
}
You need to return File({file_to_download}, "application/vnd.ms-excel") from your backend to the frontend and in your js file you need to update the code that is written below:
function exportToExcel() {
axios.post({path to call your controller}, null,
{
headers:
{
'Content-Disposition': "attachment; filename=XYZ.xlsx",
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
},
responseType: 'arraybuffer',
}
).then((r) => {
const path= window.URL.createObjectURL(new Blob([r.data]));
const link = document.createElement('a');
link.href = path;
link.setAttribute('download', 'XYZ.xlsx');
document.body.appendChild(link);
link.click();
}).catch((error) => console.log(error));
}
For those who'd like to implement an authenticated native download.
I'm currently developing a SPA with Axios.
Unfortunately Axios does't allow stream response type in such case.
From documentation:
// `responseType` indicates the type of data that the server will respond with
// options are: 'arraybuffer', 'document', 'json', 'text', 'stream'
// browser only: 'blob'
But I figured out a workaround as mentioned in this topic.
The trick is to send a basic Form POST containing your token and the targeted file.
"That targets a new window. Once the browser reads the attachment header on the server response, it will close the new tab and begin the download."
Here's a sample:
let form = document.createElement('form');
form.method = 'post';
form.target = '_blank';
form.action = `${API_URL}/${targetedResource}`;
form.innerHTML = `'<input type="hidden" name="jwtToken" value="${jwtToken}">'`;
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
"You may need to mark your handler as unauthenticated/anonymous so that you can manually validate the JWT to ensure proper authorization."
Which results for my ASP.NET implementation in:
[AllowAnonymous]
[HttpPost("{targetedResource}")]
public async Task<IActionResult> GetFile(string targetedResource, [FromForm] string jwtToken)
{
var jsonWebTokenHandler = new JsonWebTokenHandler();
var validationParameters = new TokenValidationParameters()
{
// Your token validation parameters here
};
var tokenValidationResult = jsonWebTokenHandler.ValidateToken(jwtToken, validationParameters);
if (!tokenValidationResult.IsValid)
{
return Unauthorized();
}
// Your file upload implementation here
}
This Worked for me. i implemented this solution in reactJS
const requestOptions = {`enter code here`
method: 'GET',
headers: { 'Content-Type': 'application/json' }
};
fetch(`${url}`, requestOptions)
.then((res) => {
return res.blob();
})
.then((blob) => {
const href = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = href;
link.setAttribute('download', 'config.json'); //or any other extension
document.body.appendChild(link);
link.click();
})
.catch((err) => {
return Promise.reject({ Error: 'Something Went Wrong', err });
})
I had an issue where transferring one file I downloaded from axios const axiosResponse = await axios.get(pdf.url) to google drive googleDrive.files.create({media: {body: axiosResponse.data, mimeType}, requestBody: {name: fileName, parents: [parentFolder], mimeType}, auth: jwtClient}) uploaded a corrupted file.
The reason the file was corrupted was because axios transformed the axiosResponse.data to a string. To solve the issue, I had to ask axios to return a stream axios.get(pdf.url, { responseType: 'stream' }).
Implement an Axios handler for the Received document, the data format octect-stream,
data might look weird PK something JbxfFGvddvbdfbVVH34365436fdkln as its octet stream format, you might end up creating file with this data might be corrupt, {responseType: 'blob'} will make data into readable format,
axios.get("URL", {responseType: 'blob'})
.then((r) => {
let fileName = r.headers['content-disposition'].split('filename=')[1];
let blob = new Blob([r.data]);
window.saveAs(blob, fileName);
}).catch(err => {
console.log(err);
});
you might have tried solution which fails like this,
window.saveAs(blob, 'file.zip') will try to save file as zip but will wont work,
const downloadFile = (fileData) => {
axios.get(baseUrl+"/file/download/"+fileData.id)
.then((response) => {
console.log(response.data);
const blob = new Blob([response.data], {type: response.headers['content-type'], encoding:'UTF-8'});
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'file.zip';
link.click();
})
.catch((err) => console.log(err))
}
const downloadFile = (fileData) => {
axios.get(baseUrl+"/file/download/"+fileData.id)
.then((response) => {
console.log(response);
//const binaryString = window.atob(response.data)
//const bytes = new Uint8Array(response.data)
//const arrBuff = bytes.map((byte, i) => response.data.charCodeAt(i));
//var base64 = btoa(String.fromCharCode.apply(null, new Uint8Array(response.data)));
const blob = new Blob([response.data], {type:"application/octet-stream"});
window.saveAs(blob, 'file.zip')
// const link = document.createElement('a');
// link.href = window.URL.createObjectURL(blob);
// link.download = 'file.zip';
// link.click();
})
.catch((err) => console.log(err))
}
function base64ToArrayBuffer(base64) {
var binaryString = window.atob(base64);
var binaryLen = binaryString.length;
var bytes = new Uint8Array(binaryLen);
for (var i = 0; i < binaryLen; i++) {
var ascii = binaryString.charCodeAt(i);
bytes[i] = ascii;
};
return bytes;
}
another short solution is,
window.open("URL")
will keep opening new tabs unnecessarily and user might have to make allow popups for work this code, what if user want to download multiple files at the same time so go with solution first or if not try for other solutions also
This function will help you to download a ready xlsx, csv etc file download. I just send a ready xlsx static file from backend and it in react.
const downloadFabricFormat = async () => {
try{
await axios({
url: '/api/fabric/fabric_excel_format/',
method: 'GET',
responseType: 'blob',
}).then((response) => {
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'Fabric Excel Format.xlsx');
document.body.appendChild(link);
link.click();
});
} catch(error){
console.log(error)
}
};
Basically, I solved the problem of the filename by reading it, if present, from the 'content-disposition' header:
const generateFile = async ({ api, url, payload }) => {
return await api({
url: url,
method: 'POST',
data: payload, // payload
responseType: 'blob'
}).catch((e) => {
throw e;
});
};
const getFileName = (fileBlob, defaultFileName) => {
const contentDisposition = fileBlob.headers.get('content-disposition');
if (contentDisposition) {
const fileNameIdentifier = 'filename=';
const filenamePosition = contentDisposition.indexOf(fileNameIdentifier);
if (~filenamePosition) {
return contentDisposition.slice(filenamePosition + fileNameIdentifier.length, contentDisposition.length).replace(/"/g,'');
}
}
return defaultFileName;
};
const downloadFile = (fileBlob, fileName) => {
const url = window.URL.createObjectURL(new Blob([fileBlob]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `${fileName}`);
document.body.appendChild(link);
link.click();
link.remove();
link.style.display = 'none';
window.URL.revokeObjectURL(url);
};
// "api" is an instance of Axios (axios.create)
// "payload" is the payload you submit to the server
const fileBlob = await generateFile({ api, '/url/to/download', payload });
const fileName = getFileName(fileBlob, "MyDownload.xls");
downloadFile(fileBlob.data, fileName);
For axios POST request, the request should be something like this:
The key here is that the responseType and header fields must be in the 3rd parameter of Post. The 2nd parameter is the application parameters.
export const requestDownloadReport = (requestParams) => async dispatch => {
let response = null;
try {
response = await frontEndApi.post('createPdf', {
requestParams: requestParams,
},
{
responseType: 'arraybuffer', // important...because we need to convert it to a blob. If we don't specify this, response.data will be the raw data. It cannot be converted to blob directly.
headers: {
'Content-Type': 'application/json',
'Accept': 'application/pdf'
}
});
}
catch(err) {
console.log('[requestDownloadReport][ERROR]', err);
return err
}
return response;
}
The answers using URL.CreateObject() have worked well for me.
I still want to point out the option of using HTTP Headers.
Using HttpHeaders has these advantages:
very widespread browser support
does not require creating a blob object in the browser's memory
does not require waiting for the full response from the server before showing giving the user feedback
no size limitations
Using HttpHeaders requires you to have access to the back-end server where the files are downloaded from (which seems to be the case for OP's Excel files)
HttpHeaders solution:
FRONT-END:
//...
// the download link
<a href="download/destination?parameter1=foo&param2=bar">
click me to download!
</a>
BACK-END
(C# in this example, but could be any language. Adapt as required)
...
var fs = new FileStream(filepath, FileMode.OpenOrCreate, FileAccess.Read);
Response.Headers["Content-Disposition"] = "attachment; filename=someName.txt";
return File(fs, "application/octet-stream");
...
This solution assumes you have control of the back-end server that responds.
https://github.com/eligrey/FileSaver.js/wiki/Saving-a-remote-file#using-http-header
My answer is a total hack- I just created a link that looks like a button and add the URL to that.
<a class="el-button"
style="color: white; background-color: #58B7FF;"
:href="<YOUR URL ENDPOINT HERE>"
:download="<FILE NAME NERE>">
<i class="fa fa-file-excel-o"></i> Excel
</a>
I'm using the excellent VueJs hence the odd anotations, however, this solution is framework agnostic. The idea would work for any HTML based design.

How send string/image base64 to Sailsjs - Skipper with ajax

Currently I am capturing the image of the camera, this Base64 format,and I'm sending through ajax.
xhr({
uri: 'http://localhost:1337/file/upload',
method: 'post',
body:'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAA...'
}
0 file(s) uploaded successfully!
Here is a nice link that will guide you to do send an image from an Ajax Client to an ajax server.
http://www.nickdesteffen.com/blog/file-uploading-over-ajax-using-html5
You can read this sails documentation to receive files on a sails server :
http://sailsjs.org/documentation/reference/request-req/req-file
You can do as the following example :
Client side ( ajax ):
var files = [];
$("input[type=file]").change(function(event) {
$.each(event.target.files, function(index, file) {
var reader = new FileReader();
reader.onload = function(event) {
object = {};
object.filename = file.name;
object.data = event.target.result;
files.push(object);
};
reader.readAsDataURL(file);
});
});
$("form").submit(function(form) {
$.each(files, function(index, file) {
$.ajax({url: "/ajax-upload",
type: 'POST',
data: {filename: file.filename, data: file.data}, // file.data is your base 64
success: function(data, status, xhr) {}
});
});
files = [];
form.preventDefault();
});
Server side ( sails ) :
[let's say you have a model Picture that take an ID and a URL]
[here is a sample of Picture controller, just to give you an idea]
module.exports = {
uploadPicture: function(req, res) {
req.file('picture').upload({
// don't allow the total upload size to exceed ~10MB
maxBytes: 10000000
},
function onDone(err, uploadedFiles) {
if (err) {
return res.negotiate(err);
}
// If no files were uploaded, respond with an error.
if (uploadedFiles.length === 0){
return res.badRequest('No file was uploaded');
}
// Save the "fd" and the url where the avatar for a user can be accessed
Picture
.update(777, { // give real ID
// Generate a unique URL where the avatar can be downloaded.
pictureURL: require('util').format('%s/user/pictures/%s', sails.getBaseUrl(), 777), // GIVE REAL ID
// Grab the first file and use it's `fd` (file descriptor)
pictureFD: uploadedFiles[0].fd
})
.exec(function (err){
if (err) return res.negotiate(err);
return res.ok();
});
});
}
};
Hope this will help in your research.
I also recommand you to use Postman to test your API first, then code your client.

Problems with security policy for Filepicker Convert?

I use Filepicker to "read" then "store" an image from clients' computer. Now I want to resize the image using Filepicker but always get a 403 error:
POST https://www.filepicker.io/api/file/w11b6aScR1WRXKFbcXON/convert?_cacheBust=1380818787693 403 (FORBIDDEN)
I am using the same security policy and signature for the "read", "store", and "convert" calls. Is this wrong? Because when "read" and "store" are called there is no file handle yet (e.g. the last string part in InkBlob.url). But it seems the "convert" policy/signature must be generated using the file handle returned with the "store" InkBlob? And if this is the case, what's a more convenient way to do in javascript? Because in "convert" I have no access to the Python function that generates security policies unless I write an API call for that.
My code snippet as below (initialFpSecurityObj was pre-generated in Python using an empty handle):
filepicker.store(thumbFile, {
policy: initialFpSecurityObj.policy,
signature: initialFpSecurityObj.signature,
location: "S3",
path: 'thumbs/' + initialFpSecurityObj.uniqueName + '/',
},function(InkBlob){
console.log("Store successful:", JSON.stringify(InkBlob));
processThumb(InkBlob);
}, function(FPError){
console.error(FPError.toString());
});
var processThumb = function(InkBlob){
filepicker.convert(InkBlob, {
width: 800,
height: 600,
format: "jpg",
policy: initialFpSecurityObj.policy,
signature: initialFpSecurityObj.signature,
}, function(InkBlob){
console.log("thumbnail converted and stored at:", InkBlob);
}, function(FPError){
console.error(FPError);
};
}
Thanks a lot for the help.
--- EDIT ---
Below is the snippet for the Python code that generates initialFpSecurityObj
def generateFpSecurityOptions(handle, userId, policyLife=DEFAULT_POLICY_LIFE):
expiry = int(time() + policyLife)
json_policy = json.dumps({'handle': handle, 'expiry': expiry})
policy = base64.urlsafe_b64encode(json_policy)
secret = 'XXXXXXXXXXXXXX'
signature = hmac.new(secret, policy, hashlib.sha256).hexdigest()
uniqueName = hashlib.md5()
uniqueName.update(signature + repr(time()))
uniqueName = uniqueName.hexdigest() + str(userId)
return {'policy':policy, 'signature':signature, 'expiry':expiry, 'uniqueName':uniqueName}
fp_security_options = generateFpSecurityOptions(None, request.user.id)
Then in the django template fp_security_options is retrieved:
var initialFpSecurityObj = {{fp_security_options|as_json|safe}};
The way that generates fp_security_options is suspicious to me (former colleague's code) because the handle is None.
My recommendation would be to create two policies: one that is handle-bound and allows storing of the file, and another that is not handle-bound for the convert. In this case, you can set a shorter expiry time to increase the level of security, given that you are not specifying a handle.
Your problem is probably that your policy does not contain any "call" specifications. I suggest:
json_policy = json.dumps({'handle': handle, 'expiry': expiry, 'call':['pick','store','read','convert']})
but as our (very busy ;) brettcvz suggests, for conversion only, this is already enough:
json_policy = json.dumps({'handle': handle, 'expiry': expiry, 'call':'convert'})
You can find this in the security docs https://developers.inkfilepicker.com/docs/security/
If you still have issues, use a REST call, it's free. The following method is JavaScript and returns an url to the REST endpoint of filepicker which can be used to retrieve the converted image. The _options object looks like this
var myOptions = {
w: 150,
h: 150,
fit: "crop",
align: "faces",
format: "jpg",
quality: 86
};
and will work with all parameters specified of file pickers REST-API (check out https://developers.inkfilepicker.com/docs/web/#inkblob-images).
function getConvertedURL(_handle, _options, _policy, _signature) {
// basic url piece
var url = "https://www.filepicker.io/api/file/" + _handle + "/convert?";
// appending options
for (var option in _options) {
if (_options.hasOwnProperty(option)) {
url += option + "=" + _options[option] + "&";
}
}
// appending signed policy
url += "signature=" + _signature + "&policy=" + _policy;
return url;
}
So I finally figured it out myself, although I saw brettcvz's suggestion afterwards. The key is for 'convert' to work, I have to specify the exact handle of the uploaded file (i.e. the last bit of the string in InkBlob's url property returned from the 'store' or 'pickAndStore' call.
First thing I did was to edit the Python function generating the security policy and signature:
def generateFpSecurityOptions(handle, userId, policyLife=DEFAULT_POLICY_LIFE):
expiry = int(time() + policyLife)
json_policy = json.dumps({'handle': handle, 'expiry': expiry})
policy = base64.urlsafe_b64encode(json_policy)
secret = 'XXXXXXXXXXXXXX'
signature = hmac.new(secret, policy, hashlib.sha256).hexdigest()
if not handle == None:
uniqueName = handle
else:
uniqueName = hashlib.md5()
uniqueName.update(signature + repr(time()))
uniqueName = uniqueName.hexdigest() + str(userId)
return {'policy':policy, 'signature':signature, 'expiry':expiry, 'uniqueName':uniqueName}
fp_security_options = generateFpSecurityOptions(None, request.user.id)
Then I have to established the API call in our Django framework to get this security policy object dynamically via AJAX. I am fortunate that my colleague has previously written it. So I just call the API function in Javascript to retrieve the file-specific security policy object:
var initialFpSecurityObj = {{fp_security_options|as_json|safe}};
filepicker.store(thumbFile, {
policy: initialFpSecurityObj.policy,
signature: initialFpSecurityObj.signature,
access: "public"
}, function(InkBlob) {
processThumb(InkBlob);
}, function(FPError) {
console.error(FPError.toString());
}, function(progress) {
console.log("Loading: " + progress + "%");
});
var processThumb = function(InkBlob) {
var fpHandle = InkBlob.url.split('/').pop();
$.ajax({
url: API_BASE + 'file_picker_policy',
type: 'GET',
data: {
'filename': fpHandle
},
dataType: 'json',
success: function(data) {
var newFpSecurityObj = data.data;
filepicker.convert(InkBlob, {
width: 800,
height: 600,
format: "jpg",
policy: newFpSecurityObj.policy,
signature: newFpSecurityObj.signature,
}, {
location: "S3",
path: THUMB_FOLDER + '/' + newFpSecurityObj.uniqueName + '/',
}, function(fp) { // onSuccess
console.log("successfully converted and stored!");
// do what you want with the converted file
}, function(FPError) { // onError
console.error(FPError);
});
},
failure: function() {
alert("There was an error converting the thumbnail! Please try again.");
}
});
};