How to make a complex file download functionality with Blazor server? - c#

I have a Blazor server application. I want to allow the user to download files but the content of the files needs to be built dynamically.
Basically the application shows reports to the user based on filters and etc. and I want the user to have the option to download whatever he is currently seeing.
I know I can make a "link button" that points to a Razor page that returns some sort of FileContentResult in its OnGet method but I have no idea how to pass any data to that so that the correct report file can be built.
I know there is an alternative that uses JavaScript but, as far as I know, it's more cumbersome and I'm not sure if it is any better.
I thought about doing a request to some sort of REST/WebAPI (which would allow me to pass arguments and stuff) but I cannot seem to get a WebAPI and Blazor Server projects run at the same time. The only partial success I've had is adding a WebAPI project to my Blazor Server solution and starting both. But then, while debugging, for some reason, both processes stop when I download the file.
Also the application must be hosted on Azure Web app and am not sure how feasible it would be to run both projects at the same time.
So, how can I make my Blazor Server allow the user to download a file but generate the file dynamically based on what the user is seeing on his browser?

The JavaScript alternative is very straightforward.
export function saveAsFile(filename, bytesBase64) {
if (navigator.msSaveBlob) {
//Download document in Edge browser
var data = window.atob(bytesBase64);
var bytes = new Uint8Array(data.length);
for (var i = 0; i < data.length; i++) {
bytes[i] = data.charCodeAt(i);
}
var blob = new Blob([bytes.buffer], { type: "application/octet-stream" });
navigator.msSaveBlob(blob, filename);
}
else {
var link = document.createElement('a');
link.download = filename;
link.href = "data:application/octet-stream;base64," + bytesBase64;
document.body.appendChild(link); // Needed for Firefox
link.click();
document.body.removeChild(link);
}
}
create a bytestream of whatever content you want to create and call this function through IJSInterop. I have used it in blazor server and it works well.
Please note : I found this piece of code online but I don't remember from where. I will be happy to give credit to the original author if someone knows the source.

Related

How to download a file from a shared folder in asp.net using <a href>?

Background:
There is an asp.Net MVC application, which I am currently re-creating without using MVC.
In the MVC application, the <a> elements that enables users to download a document is like:
download
and it works when I click download, because I could use a method like below in MVC:
public async Task<IActionResult> Download(string path)
{
var memory = new MemoryStream();
using (var stream = new FileStream(path, FileMode.Open))
{
await stream.CopyToAsync(memory);
}
memory.Position = 0;
return File(memory, GetContentType(path), Path.GetFileName(path));
}
Note that there is an HTML table with 500+ rows in the page, some data has a file to be downloaded and for only those rows, the href link exists in a specific cell.
Here, when I try to visit http://myapplication/api/file/2187/filter.jpg, I could see the link exists.
Problem:
I couldn't use the same Download method in .net only Application because IActionResult looks for the namespace Microsoft.AspNet.MVC. Tried to set the <a> element in my asp.net application like below:
download
However, this does not work in Chrome giving me the error: Not allowed to load local resource
I couldn't find a way to create a link on http for the file like the way it is in the MVC application. How can I enable a link in the application like: http://myapplication/api/file/2187/filter.jpg to use in the href?
Note: The files to be downloaded are located in a Shared folder, can't move them into my local PC or can't import them inside the project etc..
Note 2: There should be no page-refresh after downloading the file.
Any help or advice should be appreciated! Thanks.
File Paths Should Not Be Accepted From URL or Query String:
I would strongly recommending not using filepath in the querystring or URL.
I see in the code that you are not validating the filePath and directly downloading a file. This can be very dangerous. People can try downloading web.config or any other file on your web server if the permissions are not set correctly or if there is no other mechanism to block such request.
I would strongly recommend using Indirection for file download.
What is Indirection ?
In indirection, you would create an identifier for the file. It may be a GUID as well.
Then you can accept this GUID through QueryString or through URL.
Then in your action you would map it to right file and then download it.
Code Example:
Below code example, uses file ID.
When the file id is passed, it tries to get the file details in CustomDocumentObj.
These details contains actual file path.
You can then perform validations to check if this is the application related file and if your application allows to download that file.
public class ServiceController : Controller
{
public ActionResult DownloadFile(string id)
{
CustomDocumentObj document = new CustomDocumentObj(Int32.Parse(id));
// OPTIONAL - validation to check if it is allowed to download this file.
string filetype = Helpers.GetMimeType(document.FilePath);
return File(document.FilePath, filetype, Path.GetFileName(document.FilePath));
}
}
Then in URL it can be something like below:
Download File
Refer this blog for further details.

How to manipulate a url to access a parent directory

I have an asp.net web application that is being hosted on an internal network. In my testing environment of course it gets hosted out on localhost:01010/Views/page.aspx. now whenever I take it live the Url changes to server_name/folder 1/folder 2/views/page.aspx. what I am trying to do is get a new page to open up as server_name/folder 1/folder 2/Uploaded_Images/randomimage.png. Now I Can get the url, but as soon as I do a single ".Remove(url.lastindexof("/")+1)" it returns "server_name/folder 1/folder 2/Views". The I perform my second ".Remove(url.lastindexof("/")+1)"
and the it only returns "server_name/". I am ripping my hair out at this one and am hoping somewhere in the world a .net developer already has this built in. Appreciate all the help.
Also just to specify this is webforms and not mvc. also there is no ajax or page manipulation going on except for a response.write to open the new page.
You don't need the +1, this works:
var url = "server_name/folder 1/folder 2/views/page.aspx";
url = url.Remove(url.LastIndexOf("/"));
url = url.Remove(url.LastIndexOf("/"));
Or you could do it like this:
var parts = url.Split('/');
var newPath = string.Join("/", parts.Take(3));
I assume you are talking about URL's used as links to parts of your site and not physical paths on the file system.
In most cases, you should be able to use methods that construct paths on the fly. For example, in any of your .aspx files (or .aspx.cs files), you can use the ResolveUrl method, like this:
Some link
If there are any places where you need the full URL including the domain (like for email notifications or something like that) then what I have done is keep a static variable accessible to my whole application that gets set the first time Application_BeginRequest runs:
void Application_BeginRequest(object sender, EventArgs e) {
if (SiteRoot == null) {
SiteRoot = HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) +
(VirtualPathUtility.ToAbsolute("~") == "/" ? "" : VirtualPathUtility.ToAbsolute("~"));
}
}
That will pull the full URL from the Request details (the URL that the user used to access your site), without any trailing slash.

PhantomJs save webpage containing dynamic data as webpage

I have used jQuery plugin to generate charts on screen along with dynamic data and notes.
I need to generate PDF of this webpage on button click.
I have tried to search good option but not able to find any proper one.
Right now I am trying to use phantomjs for this. It works for internet sites but not working for intranet sites.
I am continuously getting Unable to load address.
Can anyone suggest me the way to achieve this? Or any alternative way to generate PDF from html content (My html contains SVG, Javascript generated dynamic data too).
Code:
string serverPath = "C:\\Phantomjs\\bin\\phantomjs";
var phantomJS = new PhantomJS();
var outFile = Path.Combine(serverPath, "google2.pdf");
if (File.Exists(outFile))
File.Delete(outFile);
try {
phantomJS.Run(Path.Combine(serverPath, "rasterize.js"),
new[] { "http://localhost:61362/HT.aspx?FNo=D1&PrNo=Dummy1&HtId=1033", outFile });
} finally {
phantomJS.Abort();
}
The url is not working in this case. But if I take any internet hosted site, it works.

Using the Azure API, how do I list and add virtual directories to a website?

I am trying to add a Virtual Directory to an Azure Web Site from a WinForms Application using the Azure API. I can enumerate the WebSites on in my webspace, but I cannot find a method that allows me access to the Virtual Directories in the WebSite.
Here is my code:
string certPath = Properties.Settings.Default.AzureCertificatePath;
string certPassword = Properties.Settings.Default.AzureCertificatePassword;
string subscriptionId = Properties.Settings.Default.AzureSubscriptionId;
var cert = new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.MachineKeySet);
var cred = new CertificateCloudCredentials(subscriptionId, cert);
using (var client = new WebSiteManagementClient(cred))
{
var spaces = client.WebSpaces.List();
foreach (var space in spaces)
{
Console.WriteLine("Space: {0}", space.Name);
var sites = client.WebSpaces.ListWebSites(space.Name, new WebSiteListParameters {PropertiesToInclude = { "Name" } }); ***// Where do I find out what properties can be included in this array?***
foreach (var site in sites)
{
***// What goes here to show the virtual directories in this specific website??????***
}
}
}
I found that the Azure web services API does not offer access to the virtual directories/applications on a web app, although the underlying REST API's that it uses does.
Luckily, the management API is open source (I wanted to get the v3.0.0.0 of the website management API, matching what I had NuGet'd, which I found here: https://github.com/Azure/azure-sdk-for-net/commits/master?page=79) so with a little perseverance and messing about with .targets files and NuGet references, you can get the source code of the WebSiteManagement project into your solution instead of the referenced DLL that you likely downloaded from NuGet.
From there, if you go into your client.WebSites.GetConfiguration method, stick a breakpoint in and capture the HTTP response returned - you'll see that in the JSON the VirtualApplications on your website are indeed there.
From there, you can edit your copy of the source code to expose those object structures out, much the same way that other object structures are mapped out of the JSON (e.g. HandlerMappings).
Likewise for updating them, you need to add the VirtualApplications (in the ewact same format) into the Microsoft.WindowsAzure.Management.WebSites.Models.WebSiteUpdateConfigurationParameters, and pass that into client.WebSites.UpdateConfiguration (which you will also need to amend to map your VirtualApplications object structure back into JSON in the same format).
Works great for me doing it this way.
Note/Disclaimer: The GitHub site documentation does mention that much of the source code was autogenerated, and that you shouldn't really go editing that code, instead raise an issue on the project about getting it sorted. I didn't have the time to do this and needed an immediate way of getting it working with my local copy of the code only, so my solution does, as you'll note when you look a the source, involve adding to that auto-gen'd code. It works great, but I should in good conscience point you to the project owner's warnings about tinkering with the auto-gen'd code.

C# Open web page in default browser with post data

I am sure this must have been answered before but I cannot find a solution, so I figure I am likely misunderstanding other people's solutions or trying to do something daft, but here we go.
I am writing an add-in for Outlook 2010 in C# where a user can click a button in the ribbon and submit the email contents to a web site. When they click the button the website should open in the default browser, thus allowing them to review what has just been submitted and interact with it on the website. I am able to do this using query strings in the URL using:
System.Diagnostics.Process.Start("http://www.test.com?something=value");
but the limit on the amount of data that can be submitted and the messy URLs are preventing me from following through with this approach. I would like to use an HTTP POST for this as it is obviously more suitable. However, the methods I have found for doing this do not seem to open the page up in the browser after submitting the post data:
http://msdn.microsoft.com/en-us/library/debx8sh9.aspx
to summarise; the user needs to be able to click the button in the Outlook ribbon, have the web browser open and display the contents of the email which have been submitted via POST.
EDIT:
Right, I found a way to do it, its pretty fugly but it works! Simply create a temporary .html file (that is then launched as above) containing a form with hidden fields for all the data, and have it submitted on page load with JavaScript.
I don't really like this solution as it relies on JavaScript (I have a <noscript> submit button just in case) and seems like a bit of a bodge, so I am still really hoping someone on here will come up with something better.
This is eight years late, but here's some code that illustrates the process pretty well:
string tempHTMLLocation = "some_arbitrary_location" + "/temp.html";
string url = https://your_desired_url.com";
// create the temporary html file
using (FileStream fs = new FileStream(tempHTMLLocation, FileMode.Create)) {
using (StreamWriter w = new StreamWriter(fs, Encoding.UTF8)) {
w.WriteLine("<body onload=\"goToLink()\">");
w.WriteLine("<form id=\"form\" method=\"POST\" action=\"" + url + "\">");
w.WriteLine("<input type=\"hidden\" name=\"post1\" value=\"" + post_data1 + "\">");
w.WriteLine("<input type=\"hidden\" name=\"post2\" value=\"" + post_data2 + "\">");
w.WriteLine("</form>");
w.WriteLine("<script> function goToLink() { document.getElementById(\"form\").submit(); } </script>");
w.WriteLine("</body>");
}
}
// launch the temp html file
var launchProcess = new ProcessStartInfo {
FileName = tempHTMLLocation,
UseShellExecute = true
};
Process.Start(launchProcess);
// delete temp file but add delay so that Process has time to open file
Task.Delay(1500).ContinueWith(t=> File.Delete(tempHTMLLocation));
Upon opening the page, the onload() JS script immediately submits the form, which posts the data to the url and opens it in the default browser.
The Dropbox client does it the same ways as you mentioned in your EDIT. But it also does some obfuscation, i.e. it XORs the data with the hash submitted via the URL.
Here are the steps how Dropbox does it:
in-app: Create a token that can be used to authorize at dropbox.com.
in-app: Convert token to hex string (A).
in-app: Create a secure random hex string (B) of the same length.
in-app: Calculate C = A XOr B.
in-app: Create temporary HTML file with the following functionality:
A hidden input field which contains value B.
A submit form with hidden input fields necessary for login to dropbox.com.
A JS function that reads the hash from URI, XORs it with B and writes the result to the submit forms hidden fields.
Delete hash from URI.
Submit form.
in-app: Open the temporary HTML file with the standard browser and add C as hash to the end of the URI.
Now if your browser opens the HTML file it calculates the auth token from the hidden input field and the hash in the URI and opens dropbox.com. And because of Point 5.4. you are not able to hit the back button in your browser to login again because the hash is gone.
I'm not sure I would have constructed the solution that way. Instead, I would post all the data to a web service (using HttpWebRequest, as #Loci described, or just importing the service using Visual Studio), which would store the data in a database (perhaps with a pending status). Then direct the user (using your Process.Start approach) to a page that would display the pending help ticket, which would allow them to either approve or discard the ticket.
It sounds like a bit more work, but it should clean up the architecture of what you are trying to do. Plus you have the added benefit of not worrying about how to trigger a form post from the client side.
Edit:
A plain ASMX web service should at least get you started. You can right-click on your project and select Add Service Reference to generate the proxy code for calling the service.

Categories