aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala/com/drivergrp/core/file.scala
blob: 20bd36eb2d6c476f9fe1c413eba303269226d55a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package com.drivergrp.core

import java.io.File
import java.nio.file.{Path, Paths}
import java.util.UUID._

import com.amazonaws.services.s3.AmazonS3
import com.amazonaws.services.s3.model.{Bucket, GetObjectRequest, ListObjectsV2Request}
import com.drivergrp.core.revision.Revision
import com.drivergrp.core.time.Time

import scala.concurrent.{ExecutionContext, Future}
import scalaz.{ListT, OptionT}

object file {

  final case class FileLink(
      name: Name[File],
      location: Path,
      revision: Revision[File],
      lastModificationDate: Time
  )

  trait FileService {

    def getFileLink(id: Name[File]): FileLink

    def getFile(fileLink: FileLink): File
  }

  trait FileStorage {

    def upload(localSource: File, destination: Path): Future[Unit]

    def download(filePath: Path): OptionT[Future, File]

    def delete(filePath: Path): Future[Unit]

    def list(path: Path): ListT[Future, FileLink]

    /** List of characters to avoid in S3 (I would say file names in general)
      *
      * @see http://stackoverflow.com/questions/7116450/what-are-valid-s3-key-names-that-can-be-accessed-via-the-s3-rest-api
      */
    private val illegalChars = "\\^`><{}][#%~|&@:,$=+?; "

    protected def checkSafeFileName[T](filePath: Path)(f: => T): T = {
      filePath.toString.find(c => illegalChars.contains(c)) match {
        case Some(illegalCharacter) =>
          throw new IllegalArgumentException(s"File name cannot contain character `$illegalCharacter`")
        case None => f
      }
    }
  }

  class S3Storage(s3: AmazonS3, bucket: Name[Bucket], executionContext: ExecutionContext) extends FileStorage {
    implicit private val execution = executionContext

    def upload(localSource: File, destination: Path): Future[Unit] = Future {
      checkSafeFileName(destination) {
        val _ = s3.putObject(bucket, destination.toString, localSource).getETag
      }
    }

    def download(filePath: Path): OptionT[Future, File] =
      OptionT.optionT(Future {
        val tempDir             = System.getProperty("java.io.tmpdir")
        val randomFolderName    = randomUUID().toString
        val tempDestinationFile = new File(Paths.get(tempDir, randomFolderName, filePath.toString).toString)

        if (!tempDestinationFile.getParentFile.mkdirs()) {
          throw new Exception(s"Failed to create temp directory to download file `$tempDestinationFile`")
        } else {
          Option(s3.getObject(new GetObjectRequest(bucket, filePath.toString), tempDestinationFile)).map { _ =>
            tempDestinationFile
          }
        }
      })

    def delete(filePath: Path): Future[Unit] = Future {
      s3.deleteObject(bucket, filePath.toString)
    }

    def list(path: Path): ListT[Future, FileLink] =
      ListT.listT(Future {
        import scala.collection.JavaConverters._
        val req = new ListObjectsV2Request().withBucketName(bucket).withPrefix(path.toString).withMaxKeys(2)

        def isInSubFolder(path: Path)(fileLink: FileLink) =
          fileLink.location.toString.replace(path.toString + "/", "").contains("/")

        Iterator.continually(s3.listObjectsV2(req)).takeWhile { result =>
          req.setContinuationToken(result.getNextContinuationToken)
          result.isTruncated
        } flatMap { result =>
          result.getObjectSummaries.asScala.toList.map { summary =>
            FileLink(Name[File](summary.getKey),
                     Paths.get(path.toString + "/" + summary.getKey),
                     Revision[File](summary.getETag),
                     Time(summary.getLastModified.getTime))
          } filterNot isInSubFolder(path)
        } toList
      })
  }

  class FileSystemStorage(executionContext: ExecutionContext) extends FileStorage {
    implicit private val execution = executionContext

    def upload(localSource: File, destination: Path): Future[Unit] = Future {
      checkSafeFileName(destination) {
        val destinationFile = destination.toFile

        if (destinationFile.getParentFile.exists() || destinationFile.getParentFile.mkdirs()) {
          if (localSource.renameTo(destinationFile)) ()
          else {
            throw new Exception(
                s"Failed to move file from `${localSource.getCanonicalPath}` to `${destinationFile.getCanonicalPath}`")
          }
        } else {
          throw new Exception(s"Failed to create parent directories for file `${destinationFile.getCanonicalPath}`")
        }
      }
    }

    def download(filePath: Path): OptionT[Future, File] =
      OptionT.optionT(Future {
        Option(new File(filePath.toString)).filter(file => file.exists() && file.isFile)
      })

    def delete(filePath: Path): Future[Unit] = Future {
      val file = new File(filePath.toString)
      if (file.delete()) ()
      else {
        throw new Exception(s"Failed to delete file $file" + (if (!file.exists()) ", file does not exist." else "."))
      }
    }

    def list(path: Path): ListT[Future, FileLink] =
      ListT.listT(Future {
        val file = new File(path.toString)
        if (file.isDirectory) {
          file.listFiles().toList.filter(_.isFile).map { file =>
            FileLink(Name[File](file.getName),
                     Paths.get(file.getPath),
                     Revision[File](file.hashCode.toString),
                     Time(file.lastModified()))
          }
        } else List.empty[FileLink]
      })
  }
}