Question
· Jul 24, 2017

Using %CSP.REST to serve files

Hello fellow developers,

I am currently in need of a way to serve files dynamically (sending specific replacements for when the requested file doesn't exist, possibly depending on other conditions such as passed parameters).

Being neither willing nor able to mess with the webserver itself I consider serving the files through a %CSP.REST class:

  • Parse the requested url/file, check whether that file exists
  • Check whether other conditions must be met
  • Decide which file to send
  • Send it through a file stream (is that even possible?)

As an alternative could I set the Redirect property of my response object accordingly and the webbrowser would fetch the specified file?

The rest service won't see a whole lot of usage still I wonder whether it it s a good idea. Or let me rephrase this, it certainly isn't a good idea but is it a viable one due to lack of alternatives?

Cheers!

Discussion (6)1
Log in or sign up to continue

Hi Sebastian,

> The rest service won't see a whole lot of usage still I wonder whether it is a good idea. Or let me rephrase this, it certainly isn't a good idea but is it a viable one due to lack of alternatives?

You don't have to use REST, you can use a standard CSP page (particularly for anyone using a pre REST version of Caché).

Class Foo.FileServer Extends %CSP.Page
{

ClassMethod OnPreHTTP() As %Boolean [ ServerOnly = 1 ]
{
    set filename=%request.Get("filename",1)
    set %response.ContentType=..GetFileContentType($p(filename,".",*))
    do %response.SetHeader("Content-Disposition","attachment; filename="""_$p(filename,"\",*)_"""")
    quit 1
}

ClassMethod GetFileContentType(pFileType) As %String
{
    if pFileType="pdf" quit "application/pdf"
    if pFileType="docx" quit "application/msword"
    if pFileType="txt" quit "text/plain"
    //TODO, add more MIME types...
    quit ""
}

ClassMethod OnPage() As %Status [ ServerOnly = 1 ]
{
    set filename=%request.Get("filename",1)
    set file=##class(%File).%New(filename)
    do file.Open("R")
    do file.OutputToDevice()
    Quit $$$OK
}

}


There are three things to point out.

1. You need to set the ContentType on the %response object
2. If the user is going to want to use the URL directly and download the file to a local disk, then set the content disposition, otherwise, the file name will end in .CLS
3. Simply use the OutputToDevice() method on the %File class to stream the file contents to the client.

This can be easily applied to REST, but there is a caveat that you need to look out for.

The initial REST path might look like this...

/file/:fileref


However, if you allow a full path name in the file name (including folders) then you will hit two problems

1. Security, file paths will need validating, otherwise, any file could be accessed
2. Caché REST just does not work well with \ characters or %5C characters in the URL

If you want to get around the second problem, use a different folder delimiter.

Personally, I would limit the solution to a few nick-named folders, such that the URL match would be

/file/:folder/:file


So

/file/Test/Hello.txt

would map to say C:\REST\Test\Hello.txt, or the alternative file that you would provide. Putting that together the solution would look something like...

Class Foo.RestRouter Extends %CSP.REST
{

XData UrlMap
{
<Routes>
  <Route Url="/file/:folder/:file" Method="GET" Call="GetFile" />
</Routes>
}

ClassMethod GetFile(folder, file)
{
    set filename="C:\REST\"_folder_"\"_file
    set %response.ContentType=..GetFileContentType($p(filename,".",*))
    do %response.SetHeader("Content-Disposition","attachment; filename="""_$p(filename,"\",*)_"""")
    set file=##class(%File).%New(filename)
    do file.Open("R")
    do file.OutputToDevice()
    Quit $$$OK
}

ClassMethod GetFileContentType(pFileType) As %String
{
    if pFileType="pdf" quit "application/pdf"
    if pFileType="docx" quit "application/msword"
    if pFileType="txt" quit "text/plain"
    //TODO, add more MIME types...
    quit ""
}

}

Note, if you look at Dmitry's solution the file name is added to the CGI variables which would work around some of the issues I have mentioned, but of course, the REST path would no longer keep its state if bookmarked etc.

In general, REST is fine for this type of use case.

Sean