Skip to content

Instantly share code, notes, and snippets.

@dph01
Created March 16, 2012 11:59
Show Gist options
  • Select an option

  • Save dph01/2049770 to your computer and use it in GitHub Desktop.

Select an option

Save dph01/2049770 to your computer and use it in GitHub Desktop.
def serve(reqdFile: String) : Box[LiftResponse] = {
debug("request headers: " + S.request.map( _.headers.map(h => h._1 + "\t\t" + h._2 + "\n").toString).openOr(""))
User.currentUser match {
case Full(user) => {
try {
// e.g. turn 1092345.mp4 into (1092345,mp4)
var (baseName, ext) = Media.splitExtension(reqdFile)
// should be only one in the database, check to make sure, don't want to
// show someone else's video by accident!!
val mediaList = Media.findAll(By(Media.baseNamePhysical,baseName),By(Media.fileExtension,ext))
val media = mediaList.length match {
case n: Int if n > 1 => throw new Exception("Found more than one entry in the database for logical file: " + reqdFile)
case n: Int if n == 0 => throw new Exception("Could not find media entry for logical file: " + reqdFile)
case _ => mediaList.head
}
// check to see if the user has rights to see this file
if ( ! user.media.contains(media) )
throw new Exception("User does not have access rights to: " + reqdFile)
val file: java.io.File = media.file
// taken from: http://balusc.blogspot.com/2009/02/fileservlet-supporting-resume-and.html#matches
// Prepare some variables. The ETag is an unique identifier of the file.
val eTag = media.baseNamePhysical + media.fileExtension + "_" + media.length + "_" + file.lastModified
val req = S.request.open_! // bound to have an request here
// Validate request headers for caching ---------------------------------------------------
// If-None-Match header should contain "*" or ETag. If so, then return 304.
var ifNoneMatch: Box[String] = req.header("If-None-Match")
ifNoneMatch.filterNot(matches(_ ,"ETag")).foreach( x => {
debug("returning NotModifiedResponse because If-None-Match is present but does not contain * or ETag")
return Full(new NotModifiedResponse(x))
})
// If-Modified-Since header should be greater than LastModified. If so, then return 304.
// This header is ignored if any If-None-Match header is specified.
var ifModifiedSince: Box[Long] = req.header("If-Modified-Since") .flatMap(DefaultDateTimeConverter.parseDate(_).map(_.getTime))
ifModifiedSince.foreach( x =>
if (ifNoneMatch.isEmpty && x + 1000 > file.lastModified) {
debug("returning NotModifiedResponse because If-Modified-Since is present, If-None-Match isn't and" +
" If-Modified-Since date > lastModified: " + x + " > " + file.lastModified)
return Full( new NotModifiedResponse(eTag))
}
)
// Validate request headers for resume ----------------------------------------------------
// If-Match header should contain "*" or ETag. If not, then return 412.
req.header("If-Match").filterNot(matches(_,eTag)).foreach{ x =>
debug("returning PreConditionFailed as If-Match does not contain * or ETag (" + eTag +"). It contains: "
+ x )
return Full( new PreConditionFailedResponse())
}
// If-Unmodified-Since header should be greater than LastModified. If not, then return 412.
for ( ifUnModifiedSince <- req.header("If-Unmodified-Since");
date <- DefaultDateTimeConverter.parseDate(ifUnModifiedSince);
if date.getTime + 1000L <= file.lastModified
) {
debug("returning PreConditionFailed as If-Unmodified-Since <= LastModified: " +
date.getTime + " <= " + file.lastModified )
return Full( new PreConditionFailedResponse())
}
/* Validate and process range ------------------------------------------------------------- */
// Prepare some variables. The full Range represents the complete file.
val fullRange = ReqRange(0, file.length - 1, file.length);
// Validate and process Range and If-Range headers.
debug("Range header is: " + req.header("Range").openOr("Not Set"))
var ranges: List[ReqRange] = req.header("Range").map( range => {
// Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.
if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
error("invalid format for Range header: " + range + ". Returning RequestedRangeNotSatisfiableResponse.")
return Full(new RequestedRangeNotSatisfiableResponse(file.length));
}
// the following code looks for reasons to force a return of the full file
// If-Range header should either match ETag or be greater then LastModified. If not,
// then return full file.
debug("If-Range = " + req.header("If-Range").openOr("Not Set") )
val returnFull_? = req.header("If-Range").filter( _ != eTag).map( ifRange => {
// if we're here ifRange does not match eTag, so try to parse as a date
DefaultDateTimeConverter.parseDate(ifRange).map( date => {
if ( date.getTime + 1000 < file.lastModified ) {
// If-Range is set as a date, but it is less than lastModified
debug("If-Range is set as a date (" + date.getTime +
", but it is less than lastModified (" + file.lastModified + ". Returning a full range")
true
}
else
// ifRange is set as a date and the date is greater than the last modified date
// so OK to return a range
debug("ifRange is set as a date and the date is greater than the last modified date so OK to return a range")
false
}).openOr( {
// date If-Range doesn't parse as a date either, so return full file
error("If-Range doesn't either match ETag or parse as a date so ignoring: " +
ifRange)
false
})
}).openOr( {
debug("If-Range is either not set, or matches eTag, so carry on to return the requested range.")
false // If-Range either not set, or matches eTag, so return full file
})
// this whole block returns a List[ReqRange]
if (returnFull_? == true) {
debug("returning a full range: " + fullRange )
List(fullRange)
}
else {
// Range is of format "bytes=n-n,n-n,n-n..."
var ranges = range.stripPrefix("bytes=").split(",").map( part => {
// Assuming a file with length of 100, the following examples returns bytes at:
// 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
debug("Range part: " + part)
var splitPoint = part.indexOf("-")
var a = part.take(splitPoint)
var b = part.takeRight(part.length-splitPoint-1)
var lastChar = part.length-1 // need to do the calc here as for some reason we can't do it in the
// match
var (from, to ) = if ( splitPoint == 0 ) {
(file.length - b.toLong, file.length -1) // e.g. -20 means last 20
} else if ( splitPoint == lastChar) {
(a.toLong, file.length -1 ) // e.g. 20- means from 20 to end
} else {
(a.toLong, scala.math.max(b.toLong,file.length-1)) // e.g. 10-20
}
debug("From: " + from + ", to: " + to)
ReqRange(from, to, file.length)
})
debug("found ranges: " + ranges.mkString(";"))
// Check if Range is syntactically valid. If not, then return 416.
ranges.foreach( range => {
if (range.start > range.end){
debug("returning a RequestedRangeNotSatisfiableReponse as range.start > range.end (" +
range.start + " > " + range.end )
return( Full(new RequestedRangeNotSatisfiableResponse(file.length)))
}
})
debug("Ranges OK")
ranges.toList
}
} ).openOr({
debug("Range header not set; returning full range")
List(fullRange)
})
val headers = ListBuffer[(String,String)]()
headers += ("Accept-Ranges" -> "bytes");
headers += ("Content-type" -> media.mimeType.is)
//headers += ("ETag" -> eTag);
headers += ("Last-Modified" -> Helpers.toInternetDate(file.lastModified))
// headers += ("Expires" -> Helpers.toInternetDate( System.currentTimeMillis() + 31449600000L) ) // 1 year
// headers += ("Cache-Control" -> "private; max-age=10800; pre-check=10800")
// headers += ("Pragma" -> "private")
headers += ("Content-disposition" -> (S.param("download").filter(_=="true")
.map(_ => "attachment").openOr("inline") + "; filname=" + media.displayName))
// S.param("download").filter(_=="true").foreach(x =>
// headers += ("Content-disposition" -> ("attachment; filname=" + media.displayName)))
// debug("headers: " + headers)
// val inputStream = new java.io.FileInputStream( file )
val inputStream = new java.io.RandomAccessFile(file,"r")
val (statusCode, outStreamFunc) = ranges.length match {
case 1 => {
val range = ranges.head
debug("range is : " + range + ", is full: " + range.full_? )
headers += ("Content-Range" -> ("bytes " + range.start + "-" + range.end + "/" + range.total))
headers += ("Content-Length" -> range.length.toString)
// return a partially applied function which is going to be used to write to the output stream
// later
(if (range.full_?) 200 else 206,copy(inputStream, _: OutputStream , range.start, range.length) )
}
case _ => throw new Exception("cannotohandle multiple ranges yet")
}
// val resp = StreamingResponse(inputStream,
// () => (), // onEnd
// file.length,
// headers2,
// Nil, // cookies
// 200)
val resp = OutputStreamResponse(outStreamFunc,
// file.length,
ranges.foldLeft(0L)(_ + _.length),
headers.toList,
Nil, // cookies
statusCode)
debug("... finishing in Media Server for file: " + reqdFile + " with response headers: "
+ headers.toList.map(h => h._1 + "\t\t" + h._2 + "\n").toString )
Full(resp)
}
catch {
case e => {
// TODO -decide how best to handle an exception here
error("exception caught: " + e.getMessage)
Full(new NotFoundResponse(e.getMessage))
}
}
}
case _ => {
S.error("Login to stream your video.")
val uri = S.uriAndQueryString
S.redirectTo(User.loginPageURL, ()=>{User.loginRedirect.set(uri)} )
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment