I'm working with the following Stripe.js file in a Next.js project:
import { loadStripe } from "#stripe/stripe-js";
export async function Stripe({ lineItems }, imageUrls) {
let stripePromise = null;
const getStripe = () => {
if (!stripePromise) {
stripePromise = loadStripe(process.env.NEXT_PUBLIC_API_KEY);
}
return stripePromise;
};
const stripe = await getStripe();
await stripe.redirectToCheckout({
mode: "payment",
lineItems,
successUrl: `http://localhost:3000/success?pdf=${imageUrls}`,
cancelUrl: window.location.origin,
});
}
When I call the Stripe function, I'm passing an imageUrls array which looks like this for example:
['blob:http://localhost:3000/2a47a926-be04-49a9-ad96-3279c540ebb4']
When the Stripe redirectToCheckout happens, I navigate to the success page and pass imageUrls.
My goal is to convert these imageUrls into png images from the success page using code like this inside of an async function:
const fileResponse = await fetch(imageUrl);
const contentType = fileResponse.headers.get("content-type");
const blob = await fileResponse.blob();
const imageFile = new File([blob], `someImage.png`, {
contentType,
});
I end up getting this error though:
GET blob:http://localhost:3000/2a47a926-be04-49a9-ad96-3279c540ebb4 net::ERR_FILE_NOT_FOUND
I'm guessing after the redirect this URL doesn't exist anymore? What is the correct way to make something like this work?
Edit to include success.js code:
import Layout from "../components/Layout/Layout";
import { useEffect } from "react";
function SuccessPage() {
useEffect(() => {
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
});
let value = params.pdf;
console.log("this is value");
console.log(value);
async function getFileFromUrl(imageUrl) {
const fileResponse = await fetch(imageUrl);
const contentType = fileResponse.headers.get("content-type");
const blob = await fileResponse.blob();
const ditheredImageFile = new File([blob], `test.png`, {
contentType,
});
return ditheredImageFile;
}
let imageFile = getFileFromUrl(value);
console.log("this is imageFile");
console.log(imageFile);
}, []);
return (
<Layout>
<h3>Thank You For Your Order!</h3>
</Layout>
);
}
export default SuccessPage;
You're using blobs and you shouldn't - and I'm guessing those blobs came from a user input.
Blobs in javascript work in-memory (RAM), they will be discarded when your document unloads.
Since you're redirect the user to another page (stripe) you're unloading your document and thus loosing everything you've in memory (all your blobs are gone, they are only good while your document is loaded, after you leave it/get redirected they are cleared from memory).
To solve your problem you must simply upload the documents to a server prior to unloading the document (redirecting the user to stripe) and pass your server URL instead of your "internal" (blob) URL and all should work.
Basically, you need to save your files on a server via AJAX, have the server save the files and return their URL's (or even better, an ID for your image collection) and use those server URL's on the redirect (or an ID that you'll use to retrieve all the files you need later, simplifying your parameter usage).
More info at: https://javascript.info/blob ("Blob as URL" section)
The localhost url will not work because Stripe has no way of accessing an image on your local machine. Instead, you should upload the image to a database and provide a public URL. I'm not sure what systems (e.g. AWS, Azure) you use, so it's hard to get very specific.
The database can then be queried after landing on the checkout page. One way to do this would be to pass the ID of the item as a URL param. Another way is to store information about the product in local storage.
Either way, you should get a response from your database with a unique link or ID so you know exactly what to query later.
you should pass the image data as a base64 encoded string to the success URL and then decode it in the success page to get the actual image data.
modify your code:
On the Stripe function:
const imageUrls = lineItems.map((item) => {
return btoa(item.url);
});
// ...
successUrl: `http://localhost:3000/success?pdf=${imageUrls}`,
// ...
On the SuccessPage
async function getFileFromUrl(imageUrl) {
const decodedUrl = atob(imageUrl);
const fileResponse = await fetch(decodedUrl);
const contentType = fileResponse.headers.get("content-type");
const blob = await fileResponse.blob();
const ditheredImageFile = new File([blob], `test.png`, {
contentType,
});
return ditheredImageFile;
}
I think error is coming from this one
blob url will be only available in the same origin context, but your successurl is not the same as like blob
please try to generate dataurls from blob.
const dataUrls = await Promise.all(
imageUrls.map(async (url) => {
const response = await fetch(url);
const blob = await response.blob();
return URL.createObjectURL(blob);
})
);
await stripe.redirectToCheckout({
mode: "payment",
lineItems,
successUrl: `http://localhost:3000/success?pdf=${encodeURIComponent(dataUrls.join(","))}`,
cancelUrl: window.location.origin,
});
And then please decode urls at the success page, then you could resolve your issues.
The correct way to make this work is to use a URL query parameter instead of a URL path parameter. When you pass parameters in the URL path, they are not accessible after the redirect.
So instead, you should pass the imageUrls array as a query parameter in the successUrl like this:
successUrl: http://localhost:3000/success?pdf=${imageUrls.join(',')},
You can then access the query parameter in the success page and convert the imageUrls array into png images.
Related
Context:
This is my first time working with files using Nodejs.
I am making a Youtube video downloader for personal use.
In the frontend I have multiple buttons representing a video quality, each button has attached to it a URL where the video can be found for the specified quality.
When a specific button is pressed, the function 'download' from 'client.js' is being called and gets passed the URL representing the button and a filename.
My first try was to create a write stream to the public folder of my app, and after the download was finished, get the video from the path in the frontend, but it was taking too long and it was really inefficient for big files.
The current way it works is mostly the same as the other one, but this method is even slower than the other method I used.
How can I make the download more efficient?
For example when the user presses a button, the download to right away.
client.js
const download = async (url, filename) => {
const error = document.querySelector(".error")
const status = document.querySelector(".status")
try {
status.textContent = "Downloading ..."
const response = await axios.get("/download", {params: {url, filename}})
window.location.href = response.request.responseURL
error.textContent = ""
status.textContent = "Download Complete"
}
catch(e) {
error.textContent = "Cannot Download The Data!"
status.textContent = ""
}
}
server.js
app.get('/download', async (request, response) => {
try {
const URL = request.query.url
const filename = request.query.filename
response.attachment(filename)
const { data } = await axios.get(URL, { responseType: 'stream' })
data.pipe(response)
}
catch (e) {
return response.status(404).send({error: "Url Not Found!"})
}
})
I need to get file extension from a download link, currently I'm using the following script (excerpt from React hook)
// ...
useEffect(() => {
/// ...
void (async () => {
try {
setIsLoading(true);
const response = await fetch(downloadLink);
const blob = await response.blob();
setFileExtension(fileExtensionByMimeType[blob.type]);
} finally {
setIsLoading(false);
}
})();
}, []);
// ....
This code works, but the problem is that response.blob() takes time relatively to the file size, and I guess my solution is just bad.
Maybe there is some elegant way to solve my problem?
My bad, just discovered that I have all the headers I need, I was confused with empty headers prop:
Turns out it's not empty and I can get my mime type by this code:
const response = await fetch(downloadLink, { method: 'HEAD' });
const mimeType = response.headers.get('content-type');
Also, I think HEAD is appropriate here
So the API's response contains data property which should contain the .zip file that i need. Its written in a format i do not understand.
The format:
I tried using .blob() as referenced in similar questions here on Stackoverflow, but it doesn't seem to work.
The ideal solution is this: when client presses the button, he should be prompted to download said .zip file (the one from the HTTP response) locally. I'm using axios and the request type is PUT.
My code example so far:
const exportCards = () => {
axios
.put(url, {
ids: ids,
})
.then((res) => {
return res.data.blob();
})
.then((blob) => {
var file = window.URL.createObjectURL(blob);
window.location.assign(file);
})
.catch((e) => console.log(e));
};
What I wanted:
send files of any formats from the back-end to the front-end
My tools:
axios, express, saveAs
The problem I faced with:
Unable to download zip file using axios
https://github.com/eligrey/FileSaver.js/issues/156
https://github.com/axios/axios/issues/448
Nothing helped me, probably because I did something wrong. But here is a simple and quick solution that I came up with:
//BE
const filename = "my-file-name.json";
const zip = new AdmZip();
zip.addFile(filename, body);
const content = zip.toBuffer();
res.set({
"Content-Length": Buffer.byteLength(content), //I'm not sure if this is necessary, but it's better to let it be :-)
"Content-Type": "text/plain",
"Content-Disposition": `attachment; filename=${filename}.${format}`,
});
res.status(200).send(content.toString("hex")); //my solution to the problem
//FE
const { headers, data } = await axios.post(myEndpoint);
const headerLine = headers["content-disposition"];
const filename = headerLine.replace(/[\w; ]+filename=/g, "");
const content = Buffer.from(data, "hex");
const blob = new Blob([content], { type: "application/zip" });
saveAs(blob, filename); //file-saver npm package
a tag has download attribute, in .then you can try something like that
const url = new Blob([response.data],{type:'application/zip'});
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'file.zip'); //set download attribute to link
document.body.appendChild(link);
link.click(); // this will download file.zip
I would suggest fetch over axios in concern of zip file because sometimes the response in axios request is not accurate and you might fall into currupted zip file while downloading, the best might be using a package called file-saver and fetch. I am posting this answer to help the developers to grab the concept only, the code is tested in React.
package file-saver:https://www.npmjs.com/package/file-saver
Now make any function according to your choice in react, I am assuming functional component so will write method according to functional component syntax.
note before using saveAs function you need to import from the installed package file-saver.
import { saveAs } from 'file-saver';
const downloadZipFileFromLaravel=()=>{
fetch(`your url`)
.then(res => res.blob())
.then(blob => saveAs(blob, 'Auto Photos.zip')) // saveAs is a function from the file-saver package.
.catch((err) => {
console.log(err.message);
});
}
at the end you need to connect the function with a button with onClick.
example
<button onClick={()=>downloadZipFileFromLaravel()}> </button>
Note: usage of file saver in pure javascript, you can check this:
How to use filesaver.js
For more information you can see the below discussion:
Reference: https://github.com/eligrey/FileSaver.js/issues/156
Your problem is that you didn't explicitly specify the response type in your PUT request. This should work:
const exportCards = () => {
axios
.put(url, {
ids: ids,
}, {
responseType: 'blob'
})
.then((res) => { // Now 'res.data' is Blob, not a string
var file = window.URL.createObjectURL(res.data);
window.location.assign(file);
})
.catch((e) => console.log(e));
};
If I Want to get my image from s3 but using the URL as a parameter in the params is that possible. Currently, I am getting my images using the key.
const downloadParams = {
Key: fileKey,
Bucket: BUCKET_NAME
};
const data = await s3.getObject(downloadParams);
const stream = await fs.createReadStream(data);
return await s3.getObject(downloadParams).createReadStream();
}
If you have the full image URL you can use request or a similar library to get the file instead of using s3.getObject
I'm kind of new to Redis and I'm currently experiencing a project stand-still because I don't know any other way to set and get in Redis.
My problem is I'm building a url shortener and when the user posts (a POST request) a url to the server, I'm setting the url as the key and a nanoid generated code as the value and sending back the nanoid code to the user. But when the user sends a GET request with the url code to the server I have to check if the url is already cached and redirect the user to the url but I can't because the actual url as been set as the key not the url code so it will always return undefined. Please can you help me with this problem? Is there some other to do this? Many thanks in advance! Here is the code:
import redis from 'redis';
import http from 'http';
import express from 'express';
import { Router } from 'express';
import { promisify } from 'util';
import { nanoid } from 'nanoid';
interface Handler {
(req: Request, res: Response, next: NextFunction): Promise<void> | void;
}
interface Route {
path: string;
method: string;
handler: Handler | Handler[];
}
const { PORT = 8080} = process.env;
// I'm using a docker container
const { REDIS_URL = 'redis://cache:6379' } = process.env;
const redisClient = redis.createClient({
url: REDIS_URL
});
const initCache = async () =>
new Promise((resolve, reject) => {
redisClient.on('connect', () => {
console.log('Redis client connected');
resolve(redisClient);
});
redisClient.on('error', error => reject(error));
});
async function getShortenedURL(url: string) {
const urlCode = nanoid(7);
redisClient.setex(url, 3600, urlCode);
return urlCode;
}
const getAsync = promisify(redisClient.get).bind(redisClient);
async function getFromCache(key: string) {
const data = await getAsync(key);
return data;
}
const routes = [
{
path: '/:url',
method: 'get',
handler: [
async ({ params }: Request, res: Response, next: NextFunction) => {
try {
const { url } = params;
const result = await getFromCache(url);
if (result) {
res.redirect(301, result);
} else {
throw new Error('Invalid url');
}
} catch (error) {
console.error(error);
}
}
]
},
{
path: '/api/url',
method: 'post',
handler: [
async ({ body }: Request, res: Response, next: NextFunction) => {
const { url } = body;
const result = await getFromCache(url);
result ? res.status(200).send(`http://localhost:${PORT}/${result}`) : next();
},
async ({ body }: Request, res: Response) => {
const result = await getShortenedURL(body.url as string);
res.status(200).send(result);
}
]
}
];
const applyRoutes = (routes: Route[], router: Router) => {
for (const route of routes) {
const { method, path, handler } = route;
(router as any)[method](path, handler);
}
};
const router = express();
applyRoutes(routes, router);
const server = http.createServer(router);
async function start() {
await initCache();
server.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}...`)
}
);
}
start();
As I understand, you need to make sure that you do not shorten and store any given url twice.
You could encode the url and use it as the sort version and as a key at the same time. E.g.
www.someurltoshorten.com -> encoded value ->
{key: value} -> encoded value: www.someurltoshorten.com
If a user wants to shorten a url, you encode it first and you should get the exact same hash for the exact same url.
Once you get the encoded value, you can use the SET command with a "GET" option. You can also use the expire (EXAT) option to clean up old urls (those that nobody is looking for anymore) using the feature that is built in Redis.
It will do the following for you:
Set key to hold the string value (the key is the short version of the url and the value is the url itself)
If the value exists, it will overwrite it and reset (extend) the TTL (time to live) if you set it.
And the "GET" option will return the old value if it exists or null.
With one command you will be able to:
Create a value in Redis
Get the value if it already exists resetting the TTL (it makes sense to extend it) and all of the without any extra code with one command only!!!
The flow may look as follows:
A user inputs a url to be shortened:
you encode the url
you store it in Redis using the SET command where the key is the encoded value and the value is the url.
you return the encoded value which you already now. There is no need to check whether the url has already been shortened once because the SET command will either create a new entry or update the existing once.
A user inputs a shortened url
you encode the url
you store it in Redis using the SET command where the key is the encoded value and the value is the url.
you get the url from the value that was returned by the SET command thank to the "GET" option.
The only difference between the two cases is in whether you return the shortened url or the normal url
Basically, you need one Redis command for all of that to work.
I did not test the encoding/hashing of the url and it may not work with all types of url. You need to check which encoding would cover all cases.
But the idea here is the concept itself. It's similar to how we handle passwords. When you register, it's hashed. Then, when you log in and provide the same password, we can hash it again and compare hashes. Secure hashing with bycript, as an example, can be expensive (can take a lot of time).
For urls you need to make sure that encoding/hashing always produces the same result for the same url.
Keep in mind the length of the keys as describe here https://redis.io/topics/data-types-intro#redis-keys
you should use the HashCode generate for the URL as the Key for your dictionary since you intend to lookup by the shortened URL later.
Post--> Hash the URL, Encode it as per your need for length restrictions return the shortened Key as shortened URL and put <Hash,URL> in your map
Get--> User gives the shortened Key, Dictionary lookup for shortened Key and return the actual URL.