Skip to content

Instantly share code, notes, and snippets.

@dph01
Created March 16, 2012 12:26
Show Gist options
  • Save dph01/2049865 to your computer and use it in GitHub Desktop.
Save dph01/2049865 to your computer and use it in GitHub Desktop.
media streaming
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 {
debug("starting in Media Server for logical file: " + reqdFile + ".....")
// e.g. turn 1092345.mp4 into (1092345,mp4)
var (baseName, ext) = Media.splitExtension(reqdFile)
// should be only one in the database
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
}
debug("about to serve media: " + media)
// 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 filePath = Media.mediaHomeLog + filename
debug("opening file: " + media.fileNamePhysical)
val file: java.io.File = media.file
// see: 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 not, 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 not, 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.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 = OutputStreamResponse(outStreamFunc,
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