src/main/scala/fis/fs/lib/AttachmentStore.scala
author Tomas Zeman <tzeman@volny.cz>
Tue, 23 Apr 2013 10:36:04 +0200
changeset 108 ef4e3e0ef83f
parent 100 1fcbeae1f9da
permissions -rw-r--r--
84a94fa29a67504b Task/Project notifications

/*
 * Copyright 2010-2012 Tomas Zeman <tzeman@volny.cz>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * (Originaly based on net.backup project, (c) of Tomas Zeman).
 */
package fis.fs.lib

import java.io.InputStream
import net.liftweb.common.{Box, Empty, Failure, Full, LazyLoggable}

/**
 * Generic interface contract for content retrieval/storage.
 */
trait AttachmentStore {

  /**
   * Stores content to repository.
   * @param content Content stream.
   * @return Returns content id and length.
   */
  def put(content: InputStream): Box[(String, Long)]

  /**
   * Reads content stream from repository.
   * @param id Content id.
   * @param block Function on input stream.
   * @return T func result.
   */
  def get[T](id: String)(block: InputStream => T): Box[T]

  /**
   * Reads content stream from repository.
   * Called must close the stream afterwards.
   * @param id Content id.
   * @return Content stream.
   */
  def getContent(id: String): Box[InputStream]

  /**
   * Removes content from repository.
   * @param id Content id.
   * @return True if content was successfuly removed.
   */
  def remove(id: String): Boolean

  /**
   * Checks if content <code>id</code> is in the repository.
   * @param id Content id.
   * @return True if content <code>id</code> is in the repository.
   */
  def exists(id: String): Boolean
}

import java.io.{BufferedInputStream, File, FileInputStream, FileOutputStream}
import java.nio.channels.Channels
import java.util.concurrent.atomic.AtomicLong
import net.liftweb.util.Helpers._

/**
 * Filesystem based attachment store.
 * @param baseDir Base directory. Attempts to create if does not exist.
 */
class FileSystemAttachmentStore(baseDir: File) extends AttachmentStore
  with LazyLoggable {

  /* C'tor */
  if (!baseDir.exists) {
    logger.info("Creating " + baseDir)
    baseDir.mkdirs
  }

  if (baseDir.exists && !baseDir.isDirectory)
    throw new IllegalArgumentException("baseDir %s must be a directory.".format(
    baseDir.toString))

  logger.info("Attachment store initialized in %s".format(baseDir.toString))

  /* AttachmentStore implementation methods. */

  override def put(content: InputStream): Box[(String, Long)] = for {
    (tmp, os) <- tryo {
      val f = File.createTempFile(".fscs.", ".tmp", baseDir)
      val os = new FileOutputStream(f)
      (f, os)
    }
    len <- use(os) {
      os getChannel() transferFrom(Channels newChannel content, 0, Long.MaxValue)
    }
    (id, f, renamed) <- tryo {
      val s = serial.incrementAndGet.toString
      val f = new File(baseDir, s)
      (s, f, tmp renameTo f)
    } if renamed
  } yield (id, len)

  def getContent(id: String): Box[InputStream] = withFile(id) { f =>
    new BufferedInputStream(new FileInputStream(f)) }

  def get[T](id: String)(block: InputStream => T): Box[T] =
    withStream(id)(block)

  override def remove(id: String): Boolean =
    withFile(id)(_.delete) openOr false
  
  override def exists(id: String): Boolean =
    withFile(id)(_.exists) openOr false
  
  /* Implementation private. */

  def use[T](c: { def close(): Unit })(block: => T): Box[T] = {
    val r = tryo(block)
    tryo(c.close)
    r
  }

  protected def withFile[T](id: String)(block: File => T): Box[T] = for {
    f <- getFile(id)
    r <- tryo(block(f))
  } yield r

  protected def withStream[T](id: String)(block: InputStream => T): Box[T] = (for {
    f <- getFile(id)
    is <- tryo { new BufferedInputStream(new FileInputStream(f)) }
  } yield {
    logger.debug("withStream(%s)".format(id))
    val r = tryo(block(is))
    tryo { is.close }
    r
  }) flatMap { v => v }

  protected def sanitizeId(id: String): Box[String] = for {
    i <- (Box !! id) if !i.exists(!_.isDigit)
  } yield i

  protected def getFile(id: String): Box[File] = for {
    i <- sanitizeId(id)
  } yield new File(baseDir, i)

  private lazy val serial = new AtomicLong(millis)
}


// vim: set ts=2 sw=2 et: