All Downloads are FREE. Search and download functionalities are using the official Maven repository.

quasar.fs.WriteFile.scala Maven / Gradle / Ivy

There is a newer version: 28.1.6
Show newest version
/*
 * Copyright 2014–2016 SlamData Inc.
 *
 * 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.
 */

package quasar.fs

import quasar.Predef._
import quasar._, RenderTree.ops._
import quasar.effect.LiftedOps
import quasar.fp._

import monocle.Iso
import scalaz._, Scalaz._
import scalaz.stream._

sealed trait WriteFile[A]

object WriteFile {
  final case class WriteHandle(file: AFile, id: Long)

  object WriteHandle {
    val tupleIso: Iso[WriteHandle, (AFile, Long)] =
      Iso((h: WriteHandle) => (h.file, h.id))((WriteHandle(_, _)).tupled)

    implicit val writeHandleShow: Show[WriteHandle] =
      Show.showFromToString

    // TODO: Switch to order once Order[Path[B,T,S]] exists
    implicit val writeHandleEqual: Equal[WriteHandle] =
      Equal.equalBy(tupleIso.get)
  }

  final case class Open(file: AFile)
    extends WriteFile[FileSystemError \/ WriteHandle]

  final case class Write(h: WriteHandle, chunk: Vector[Data])
    extends WriteFile[Vector[FileSystemError]]

  final case class Close(h: WriteHandle)
    extends WriteFile[Unit]

  final class Ops[S[_]](implicit S: Functor[S], val unsafe: Unsafe[S]) {
    import FileSystemError._, PathError._
    import ManageFile.MoveSemantics

    type F[A]    = unsafe.F[A]
    type M[A]    = unsafe.M[A]
    type G[E, A] = EitherT[F, E, A]

    /** Returns a channel that appends chunks of data to the given file, creating
      * it if it doesn't exist. Any errors encountered while writing are emitted,
      * all attempts are made to continue consuming input until the source is
      * exhausted.
      */
    def appendChannel(dst: AFile): Channel[M, Vector[Data], Vector[FileSystemError]] = {
      def closeHandle(h: WriteHandle): Process[M, Nothing] =
        Process.eval_[M, Unit](unsafe.close(h).liftM[FileSystemErrT])

      def writeChunk(h: WriteHandle): Vector[Data] => M[Vector[FileSystemError]] =
        xs => unsafe.write(h, xs).liftM[FileSystemErrT]

      Process.bracket(unsafe.open(dst))(closeHandle)(h => channel.lift(writeChunk(h)))
    }

    /** Same as `append` but accepts chunked [[Data]]. */
    def appendChunked(dst: AFile, src: Process[F, Vector[Data]]): Process[M, FileSystemError] = {
      val accumPartialWrites =
        process1.id[FileSystemError]
          .map(partialWrite.getOption)
          .reduceSemigroup
          .pipe(process1.stripNone)
          .map(PartialWrite)

      val dropPartialWrites =
        process1.filter[FileSystemError](e => !partialWrite.isMatching(e))

      // NB: We don't use `through` as we want to ensure the `Open` from
      //     `appendChannel` happens even if the src process never emits.
      appendChannel(dst)
        .zipWith(src.translate[M](liftMT[F, FileSystemErrT]))((f, o) => f(o))
        .eval
        .flatMap(Process.emitAll)
        .flatMap(e => Process.emitW(e) ++ Process.emitO(e))
        .pipe(process1.multiplex(dropPartialWrites, accumPartialWrites))
    }

    /** Appends data to the given file, creating it if it doesn't exist. May not
      * write all values from `src` in the presence of errors, will emit a
      * [[FileSystemError]] for each input value that failed to write.
      */
    def append(dst: AFile, src: Process[F, Data]): Process[M, FileSystemError] =
      appendChunked(dst, src map (Vector(_)))

    /** Appends the given data to the specified file, creating it if it doesn't
      * exist. May not write every given value in the presence of errors, returns
      * a [[FileSystemError]] for each input value that failed to write.
      */
    def appendThese(dst: AFile, data: Vector[Data]): M[Vector[FileSystemError]] =
      // NB: the handle will be closed even if `write` produces errors in its value
      unsafe.open(dst) flatMapF (h => unsafe.write(h, data) <* unsafe.close(h) map (_.right))

    /** Same as `save` but accepts chunked [[Data]]. */
    def saveChunked(dst: AFile, src: Process[F, Vector[Data]])
                   (implicit MF: ManageFile.Ops[S])
                   : Process[M, FileSystemError] = {

      saveChunked0(dst, src, MoveSemantics.Overwrite)
    }

    /** Write the data stream to the given path, replacing any existing contents,
      * atomically. Any errors during writing will abort the entire operation
      * leaving any existing values unaffected.
      */
    def save(dst: AFile, src: Process[F, Data])
            (implicit MF: ManageFile.Ops[S])
            : Process[M, FileSystemError] = {

      saveChunked(dst, src map (Vector(_)))
    }

    /** Write the given data to the specified file, replacing any existing
      * contents, atomically. Any errors during writing will abort the
      * entire operation leaving any existing values unaffected.
      */
    def saveThese(dst: AFile, data: Vector[Data])
                 (implicit MF: ManageFile.Ops[S])
                 : M[Vector[FileSystemError]] = {

      saveThese0(dst, data, MoveSemantics.Overwrite)
    }

    /** Same as `create` but accepts chunked [[Data]]. */
    def createChunked(dst: AFile, src: Process[F, Vector[Data]])
                     (implicit QF: QueryFile.Ops[S], MF: ManageFile.Ops[S])
                     : Process[M, FileSystemError] = {

      QF.fileExistsM(dst).liftM[Process].ifM(
        shouldNotExist(dst).liftM[Process],
        saveChunked0(dst, src, MoveSemantics.FailIfExists))
    }

    /** Create the given file with the contents of `src`. Fails if already exists. */
    def create(dst: AFile, src: Process[F, Data])
              (implicit QF: QueryFile.Ops[S], MF: ManageFile.Ops[S])
              : Process[M, FileSystemError] = {

      createChunked(dst, src map (Vector(_)))
    }

    /** Create the given file with the contents of `data`. Fails if already exists. */
    def createThese(dst: AFile, data: Vector[Data])
                   (implicit QF: QueryFile.Ops[S], MF: ManageFile.Ops[S])
                   : M[Vector[FileSystemError]] = {

      QF.fileExistsM(dst).ifM(
        shouldNotExist(dst) map (Vector(_)),
        saveThese0(dst, data, MoveSemantics.FailIfExists))
    }

    /** Same as `replace` but accepts chunked [[Data]]. */
    def replaceChunked(dst: AFile, src: Process[F, Vector[Data]])
                      (implicit QF: QueryFile.Ops[S], MF: ManageFile.Ops[S])
                      : Process[M, FileSystemError] = {

      QF.fileExistsM(dst).liftM[Process].ifM(
        saveChunked0(dst, src, MoveSemantics.FailIfMissing),
        shouldExist(dst).liftM[Process])
    }

    /** Replace the contents of the given file with `src`. Fails if the file
      * doesn't exist.
      */
    def replace(dst: AFile, src: Process[F, Data])
               (implicit QF: QueryFile.Ops[S], MF: ManageFile.Ops[S])
               : Process[M, FileSystemError] = {

      replaceChunked(dst, src map (Vector(_)))
    }

    /** Replace the contents of the given file with `data`. Fails if the file
      * doesn't exist.
      */
    def replaceWithThese(dst: AFile, data: Vector[Data])
                        (implicit QF: QueryFile.Ops[S], MF: ManageFile.Ops[S])
                        : M[Vector[FileSystemError]] = {

      QF.fileExistsM(dst).ifM(
        saveThese0(dst, data, MoveSemantics.FailIfMissing),
        shouldExist(dst) map (Vector(_)))
    }

    ////

    type GE[A] = G[FileSystemError,A]

    private def shouldNotExist(dst: AFile): M[FileSystemError] =
      MonadError[GE, FileSystemError].raiseError(pathErr(pathExists(dst)))

    private def shouldExist(dst: AFile): M[FileSystemError] =
      MonadError[GE, FileSystemError].raiseError(pathErr(pathNotFound(dst)))

    private def saveChunked0(dst: AFile, src: Process[F, Vector[Data]], sem: MoveSemantics)
                            (implicit MF: ManageFile.Ops[S])
                            : Process[M, FileSystemError] = {

      def cleanupTmp(tmp: AFile)(t: Throwable): Process[M, Nothing] =
        Process.eval_(MF.delete(tmp)).causedBy(Cause.Error(t))

      MF.tempFile(dst).liftM[Process] flatMap { tmp =>
        appendChunked(tmp, src)
          .map(some).append(Process.emit(none))
          .take(1)
          .flatMap(_.cata(
            werr => MF.delete(tmp).as(werr).liftM[Process],
            Process.eval_(MF.moveFile(tmp, dst, sem))))
          .onFailure(cleanupTmp(tmp))
      }
    }

    private def saveThese0(dst: AFile, data: Vector[Data], sem: MoveSemantics)
                          (implicit MF: ManageFile.Ops[S])
                          : M[Vector[FileSystemError]] = {
      for {
        tmp  <- MF.tempFile(dst)
        errs <- appendThese(tmp, data)
        _    <- if (errs.isEmpty) MF.moveFile(tmp, dst, sem)
                else MF.delete(tmp)
      } yield errs
    }
  }

  object Ops {
    implicit def apply[S[_]](implicit S: Functor[S], U: Unsafe[S]): Ops[S] =
      new Ops[S]
  }

  /** Low-level, unsafe operations. Clients are responsible for resource-safety
    * when using these.
    */
  @SuppressWarnings(Array("org.brianmckenna.wartremover.warts.NonUnitStatements"))
  final class Unsafe[S[_]](implicit S0: Functor[S], S1: WriteFileF :<: S)
    extends LiftedOps[WriteFile, S] {

    type M[A] = FileSystemErrT[F, A]

    /** Returns a write handle for the specified file which may be used to
      * append data to the file it represents, creating it if necessary.
      *
      * Care must be taken to `close` the handle when it is no longer needed
      * to avoid potential resource leaks.
      */
    def open(file: AFile): M[WriteHandle] =
      EitherT(lift(Open(file)))

    /** Write a chunk of data to the file represented by the write handle.
      *
      * Attempts to write as much of the chunk as possible, even if parts of
      * it fail, any such failures will be returned in the output `Vector`
      * An empty `Vector` means the entire chunk was written successfully.
      */
    def write(h: WriteHandle, chunk: Vector[Data]): F[Vector[FileSystemError]] =
      lift(Write(h, chunk))

    /** Close the write handle, freeing any resources it was using. */
    def close(h: WriteHandle): F[Unit] =
      lift(Close(h))
  }

  object Unsafe {
    implicit def apply[S[_]](implicit S0: Functor[S], S1: WriteFileF :<: S): Unsafe[S] =
      new Unsafe[S]
  }

  implicit def renderTree[A]: RenderTree[WriteFile[A]] =
    new RenderTree[WriteFile[A]] {
      def render(wf: WriteFile[A]) = wf match {
        case Open(file)           => NonTerminal(List("Open"), None, List(file.render))
        case Write(handle, chunk) =>
          NonTerminal(List("Read"), handle.toString.some,
            chunk.map(d => Terminal(List("Data"), d.toString.some)).toList)
        case Close(handle)        => Terminal(List("Close"), handle.toString.some)
      }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy