s3_website_monadic 0.0.37 → 0.0.38

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2b7836ea48c1ed8fed64738ea35590b2e7f2c0cc
4
- data.tar.gz: 4d41438c3b50479d1dad20d8baee449a35786a68
3
+ metadata.gz: df94a2c8b44f12db2fc393cd4de9126c68ba3f17
4
+ data.tar.gz: d9cd16f493ca0e4d861a7ed2b6b176cfdf58651e
5
5
  SHA512:
6
- metadata.gz: 4bc9ca2bb1226506c00eb47943057f6e2cbf86e655083f6d1d9ab42730ab30e2f0acd66d47fa2ceccdae378e85f67e48e3e4cbf3279ab289a5c78657e36ffce1
7
- data.tar.gz: e6c3ac49a91edc6643942b2005cd6f63ecce88c69f8e587aa6bf6f47b9a94fd4e79a5f0fc49deb6c0dfa125fb74613b30695a78acef0fd512ec45f5afbaf4a58
6
+ metadata.gz: 377416d509c1df1ff449da81f87888aaf86cfec2423ddb615ac974b2c2383eef32ae1c6aee373b4ae177656fa4aebc052e95e8965131e07387c138143ffbf2b8
7
+ data.tar.gz: a89614a07d46403aaf97e29b693677ee2f704fece8836721f822fa17b728465e8c8b59e2d168f8b108e997ccc6d84708d839e8db9cdcbdc924b27418e2b055c4
@@ -119,6 +119,7 @@ def autoinstall_java_or_print_help_and_exit(logger)
119
119
 
120
120
  def print_manual_method_and_exit
121
121
  @logger.info_msg 'Go to http://java.com, install Java and then try again.'
122
+ @logger.info_msg "(If you cannot or do not want to install Java, you can use latest 1.x version of this gem, which requires only Ruby. For more info, see https://github.com/laurilehmijoki/s3_website/tree/1.x)"
122
123
  exit 1
123
124
  end
124
125
 
data/build.sbt CHANGED
@@ -26,10 +26,6 @@ libraryDependencies += "org.apache.tika" % "tika-core" % "1.4"
26
26
 
27
27
  libraryDependencies += "com.lexicalscope.jewelcli" % "jewelcli" % "0.8.9"
28
28
 
29
- libraryDependencies += "joda-time" % "joda-time" % "2.3"
30
-
31
- libraryDependencies += "org.joda" % "joda-convert" % "1.2"
32
-
33
29
  libraryDependencies += "org.specs2" %% "specs2" % "2.3.11" % "test"
34
30
 
35
31
  resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"
@@ -1,3 +1,3 @@
1
1
  module S3Website
2
- VERSION = '0.0.37'
2
+ VERSION = '0.0.38'
3
3
  end
@@ -117,7 +117,7 @@ object CloudFront {
117
117
 
118
118
 
119
119
  def needsInvalidation: PartialFunction[PushSuccessReport, Boolean] = {
120
- case SuccessfulUpload(localFile, _, _) => localFile.left.exists(_.uploadType == FileUpdate)
120
+ case succ: SuccessfulUpload => succ.details.fold(_.uploadType, _.uploadType) == FileUpdate
121
121
  case SuccessfulDelete(_) => true
122
122
  case _ => false
123
123
  }
@@ -19,7 +19,7 @@ case class Diff(
19
19
 
20
20
  object Diff {
21
21
 
22
- type UploadBatch = Future[Either[ErrorReport, Seq[LocalFileFromDisk]]]
22
+ type UploadBatch = Future[Either[ErrorReport, Seq[LocalFile]]]
23
23
 
24
24
  def resolveDiff(s3FilesFuture: Future[Either[ErrorReport, Seq[S3File]]])
25
25
  (implicit site: Site, logger: Logger, executor: ExecutionContextExecutor): Either[ErrorReport, Diff] =
@@ -28,26 +28,25 @@ object Diff {
28
28
 
29
29
  private def resolveDiffAgainstGetBucketResponse(s3FilesFuture: Future[Either[ErrorReport, Seq[S3File]]])
30
30
  (implicit site: Site, logger: Logger, executor: ExecutionContextExecutor): Either[ErrorReport, Diff] = {
31
- val diffSrc = s3FilesFuture.map { errorOrS3Files =>
31
+ val diffAgainstS3 = s3FilesFuture.map { errorOrS3Files =>
32
32
  errorOrS3Files.right.flatMap { s3Files =>
33
33
  Try {
34
34
  val s3KeyIndex = s3Files.map(_.s3Key).toSet
35
35
  val s3Md5Index = s3Files.map(_.md5).toSet
36
36
  val siteFiles = Files.listSiteFiles
37
- val nameExistsOnS3 = (f: File) => s3KeyIndex contains site.resolveS3Key(f)
38
- val newFiles = siteFiles
39
- .filterNot(nameExistsOnS3)
40
- .map { f => LocalFileFromDisk(f, uploadType = NewFile)}
41
- val changedFiles =
42
- siteFiles
43
- .filter(nameExistsOnS3)
44
- .map(f => LocalFileFromDisk(f, uploadType = FileUpdate))
45
- .filterNot(localFile => s3Md5Index contains localFile.md5)
37
+ val existsOnS3 = (f: File) => s3KeyIndex contains site.resolveS3Key(f)
38
+ val isChangedOnS3 = (localFile: LocalFile) => !(s3Md5Index contains localFile.md5.get)
39
+ val newFiles = siteFiles collect {
40
+ case file if !existsOnS3(file) => LocalFile(file, NewFile)
41
+ }
42
+ val changedFiles = siteFiles collect {
43
+ case file if existsOnS3(file) => LocalFile(file, FileUpdate)
44
+ } filter isChangedOnS3
46
45
  val unchangedFiles = {
47
46
  val newOrChangedFiles = (changedFiles ++ newFiles).map(_.originalFile).toSet
48
47
  siteFiles.filterNot(f => newOrChangedFiles contains f)
49
48
  }
50
- val allFiles: Seq[Either[DbRecord, LocalFileFromDisk]] = unchangedFiles.map {
49
+ val allFiles: Seq[Either[DbRecord, LocalFile]] = unchangedFiles.map {
51
50
  f => Left(DbRecord(f))
52
51
  } ++ (changedFiles ++ newFiles).map {
53
52
  Right(_)
@@ -60,8 +59,8 @@ object Diff {
60
59
  }
61
60
  }
62
61
  }
63
- def collectResult[B](pf: PartialFunction[Either[DbRecord, LocalFileFromDisk],B]) =
64
- diffSrc.map { errorOrDiffSource =>
62
+ def collectResult[B](pf: PartialFunction[Either[DbRecord, LocalFile],B]) =
63
+ diffAgainstS3.map { errorOrDiffSource =>
65
64
  errorOrDiffSource.right map (_ collect pf)
66
65
  }
67
66
  val unchanged = collectResult {
@@ -78,16 +77,14 @@ object Diff {
78
77
  val localKeys = for {
79
78
  errorOrUnchanged <- diff.unchanged
80
79
  errorsOrChanges <- Future.sequence(diff.uploads)
81
- } yield {
80
+ } yield
82
81
  errorsOrChanges.foldLeft(errorOrUnchanged: Either[ErrorReport, Seq[S3Key]]) { (memo, errorOrChanges) =>
83
82
  for {
84
83
  mem <- memo.right
85
84
  keysToDelete <- errorOrChanges.right
86
- } yield {
87
- mem ++ keysToDelete.map(_.s3Key)
88
- }
85
+ } yield mem ++ keysToDelete.map(_.s3Key)
89
86
  }
90
- }
87
+
91
88
  s3Files zip localKeys map { (s3Files: Either[ErrorReport, Seq[S3File]], errorOrLocalKeys: Either[ErrorReport, Seq[S3Key]]) =>
92
89
  for {
93
90
  localS3Keys <- errorOrLocalKeys.right
@@ -115,13 +112,13 @@ object Diff {
115
112
 
116
113
  def resolveDiffAgainstLocalDb(s3FilesFuture: Future[Either[ErrorReport, Seq[S3File]]])
117
114
  (implicit site: Site, logger: Logger, executor: ExecutionContextExecutor): Either[ErrorReport, Diff] = {
118
- val localDiff: Either[ErrorReport, Seq[Either[DbRecord, LocalFileFromDisk]]] =
115
+ val localDiff: Either[ErrorReport, Seq[Either[DbRecord, LocalFile]]] =
119
116
  (for {
120
117
  dbFile <- getOrCreateDbFile
121
118
  databaseIndices <- loadDbFromFile(dbFile)
122
119
  } yield {
123
120
  val siteFiles = Files.listSiteFiles
124
- val recordsOrChangedFiles = siteFiles.foldLeft(Seq(): Seq[Either[DbRecord, LocalFileFromDisk]]) { (localFiles, file) =>
121
+ val recordsOrChangedFiles = siteFiles.foldLeft(Seq(): Seq[Either[DbRecord, LocalFile]]) { (localFiles, file) =>
125
122
  val truncatedKey = TruncatedDbRecord(file)
126
123
  val fileIsUnchanged = databaseIndices.truncatedIndex contains truncatedKey
127
124
  if (fileIsUnchanged)
@@ -130,7 +127,7 @@ object Diff {
130
127
  val uploadType =
131
128
  if (databaseIndices.s3KeyIndex contains truncatedKey.s3Key) FileUpdate
132
129
  else NewFile
133
- localFiles :+ Right(LocalFileFromDisk(file, uploadType))
130
+ localFiles :+ Right(LocalFile(file, uploadType))
134
131
  }
135
132
  }
136
133
  logger.debug(s"Discovered ${siteFiles.length} files on the local site, of which ${recordsOrChangedFiles count (_.isRight)} are new or changed")
@@ -145,11 +142,11 @@ object Diff {
145
142
  case Left(f) => f
146
143
  }
147
144
 
148
- val uploadsResolvedByLocalDiff = localDiffResult collect {
145
+ val uploadsAccordingToLocalDiff = localDiffResult collect {
149
146
  case Right(f) => f
150
147
  }
151
148
 
152
- val changesMissedByLocalDiff: Future[Either[ErrorReport, Seq[LocalFileFromDisk]]] = s3FilesFuture.map { errorOrS3Files =>
149
+ val changesMissedByLocalDiff: Future[Either[ErrorReport, Seq[LocalFile]]] = s3FilesFuture.map { errorOrS3Files =>
153
150
  for (s3Files <- errorOrS3Files.right) yield {
154
151
  val remoteS3Keys = s3Files.map(_.s3Key).toSet
155
152
  val localS3Keys = unchangedAccordingToLocalDiff.map(_.s3Key).toSet
@@ -157,11 +154,11 @@ object Diff {
157
154
  def isChangedOnS3(s3File: S3File) = (localS3Keys contains s3File.s3Key) && !(localMd5 contains s3File.md5)
158
155
  val changedOnS3 = s3Files collect {
159
156
  case s3File if isChangedOnS3(s3File) =>
160
- LocalFileFromDisk(site resolveFile s3File, FileUpdate)
157
+ LocalFile(site resolveFile s3File, FileUpdate)
161
158
  }
162
159
  val missingFromS3 = localS3Keys collect {
163
160
  case localS3Key if !(remoteS3Keys contains localS3Key) =>
164
- LocalFileFromDisk(site resolveFile localS3Key, NewFile)
161
+ LocalFile(site resolveFile localS3Key, NewFile)
165
162
 
166
163
  }
167
164
  changedOnS3 ++ missingFromS3
@@ -180,14 +177,14 @@ object Diff {
180
177
  val unchangedFilesFinal = errorOrDiffAgainstS3 map {
181
178
  _ fold (
182
179
  (error: ErrorReport) => Left(error),
183
- (syncResult: (Seq[DbRecord], Seq[LocalFileFromDisk])) => Right(syncResult._1)
180
+ (syncResult: (Seq[DbRecord], Seq[LocalFile])) => Right(syncResult._1)
184
181
  )
185
182
  }
186
183
 
187
184
  val changedAccordingToS3Diff = errorOrDiffAgainstS3.map {
188
185
  _ fold (
189
186
  (error: ErrorReport) => Left(error),
190
- (syncResult: (Seq[DbRecord], Seq[LocalFileFromDisk])) => Right(syncResult._2)
187
+ (syncResult: (Seq[DbRecord], Seq[LocalFile])) => Right(syncResult._2)
191
188
  )
192
189
  }
193
190
  val persistenceError: Future[Either[ErrorReport, _]] = for {
@@ -198,13 +195,13 @@ object Diff {
198
195
  records1 <- unchanged.right
199
196
  records2 <- changedAccordingToS3.right
200
197
  } yield
201
- persist(records1.map(Left(_)) ++ records2.map(Right(_)) ++ uploadsResolvedByLocalDiff.map(Right(_))) match {
198
+ persist(records1.map(Left(_)) ++ records2.map(Right(_)) ++ uploadsAccordingToLocalDiff.map(Right(_))) match {
202
199
  case Success(_) => Unit
203
200
  case Failure(err) => ErrorReport(err)
204
201
  }
205
202
  Diff(
206
203
  unchangedFilesFinal map (_.right.map(_ map (_.s3Key))),
207
- uploads = Future(Right(uploadsResolvedByLocalDiff)) :: changedAccordingToS3Diff :: Nil,
204
+ uploads = Future(Right(uploadsAccordingToLocalDiff)) :: changedAccordingToS3Diff :: Nil,
208
205
  persistenceError = persistenceError map (_.left.toOption)
209
206
  )
210
207
  }
@@ -244,13 +241,13 @@ object Diff {
244
241
  )
245
242
  }
246
243
 
247
- def persist(recordsOrChangedFiles: Seq[Either[DbRecord, LocalFileFromDisk]])(implicit site: Site, logger: Logger): Try[Seq[Either[DbRecord, LocalFileFromDisk]]] =
244
+ def persist(recordsOrChangedFiles: Seq[Either[DbRecord, LocalFile]])(implicit site: Site, logger: Logger): Try[Seq[Either[DbRecord, LocalFile]]] =
248
245
  getOrCreateDbFile flatMap { dbFile =>
249
246
  Try {
250
247
  val dbFileContents = recordsOrChangedFiles.map { recordOrChangedFile =>
251
248
  val record: DbRecord = recordOrChangedFile fold(
252
249
  record => record,
253
- changedFile => DbRecord(changedFile.s3Key, changedFile.originalFile.length, changedFile.originalFile.lastModified, changedFile.md5)
250
+ changedFile => DbRecord(changedFile.s3Key, changedFile.originalFile.length, changedFile.originalFile.lastModified, changedFile.md5.get)
254
251
  )
255
252
  record.s3Key :: record.fileLength :: record.fileModified :: record.uploadFileMd5 :: Nil mkString "|"
256
253
  } mkString "\n"
@@ -278,6 +275,6 @@ object Diff {
278
275
 
279
276
  object DbRecord {
280
277
  def apply(original: File)(implicit site: Site): DbRecord =
281
- DbRecord(site resolveS3Key original, original.length, original.lastModified, LocalFileFromDisk.md5(original))
278
+ DbRecord(site resolveS3Key original, original.length, original.lastModified, LocalFile.md5(original).get)
282
279
  }
283
280
  }
@@ -1,6 +1,8 @@
1
1
  package s3.website
2
2
 
3
- class Logger(val verboseOutput: Boolean, logMessage: (String) => Unit = println) {
3
+ import scala.util.Try
4
+
5
+ class Logger(val verboseOutput: Boolean) {
4
6
  def debug(msg: String) = if (verboseOutput) log(Debug, msg)
5
7
  def info(msg: String) = log(Info, msg)
6
8
  def fail(msg: String) = log(Failure, msg)
@@ -10,9 +12,9 @@ class Logger(val verboseOutput: Boolean, logMessage: (String) => Unit = println)
10
12
 
11
13
  def pending(msg: String) = log(Wait, msg)
12
14
 
13
- private def log(logType: LogType, msgRaw: String) {
15
+ private def log(logType: LogType, msgRaw: String): Try[Unit] = {
14
16
  val msg = msgRaw.replaceAll("\\n", "\n ") // Indent new lines, so that they arrange nicely with other log lines
15
- logMessage(s"[$logType] $msg")
17
+ Try(println(s"[$logType] $msg"))
16
18
  }
17
19
 
18
20
  sealed trait LogType {
@@ -191,7 +191,7 @@ object Push {
191
191
  (failureReport: PushFailureReport) => counts.copy(failures = counts.failures + 1),
192
192
  (successReport: PushSuccessReport) =>
193
193
  successReport match {
194
- case succ: SuccessfulUpload => succ.source.fold(_.uploadType, _.uploadType) match {
194
+ case succ: SuccessfulUpload => succ.details.fold(_.uploadType, _.uploadType) match {
195
195
  case NewFile => counts.copy(newFiles = counts.newFiles + 1).addTransferStats(succ) // TODO nasty repetition here
196
196
  case FileUpdate => counts.copy(updates = counts.updates + 1).addTransferStats(succ)
197
197
  case RedirectFile => counts.copy(redirects = counts.redirects + 1).addTransferStats(succ)
@@ -228,14 +228,14 @@ object Push {
228
228
  redirects: Int = 0,
229
229
  deletes: Int = 0,
230
230
  uploadedBytes: Long = 0,
231
- uploadDurations: Seq[org.joda.time.Duration] = Nil
231
+ uploadDurations: Seq[UploadDuration] = Nil
232
232
  ) {
233
233
  val thereWasSomethingToPush = updates + newFiles + redirects + deletes > 0
234
234
 
235
235
  def addTransferStats(successfulUpload: SuccessfulUpload): PushCounts =
236
236
  copy(
237
237
  uploadedBytes = uploadedBytes + (successfulUpload.uploadSize getOrElse 0L),
238
- uploadDurations = uploadDurations ++ successfulUpload.uploadDuration
238
+ uploadDurations = uploadDurations ++ successfulUpload.details.fold(_.uploadDuration, _ => None)
239
239
  )
240
240
  }
241
241
 
@@ -2,26 +2,17 @@ package s3.website
2
2
 
3
3
  import s3.website.model._
4
4
  import com.amazonaws.services.s3.{AmazonS3, AmazonS3Client}
5
- import com.amazonaws.auth.BasicAWSCredentials
6
5
  import com.amazonaws.services.s3.model._
7
6
  import scala.collection.JavaConversions._
8
7
  import scala.concurrent.{ExecutionContextExecutor, Future}
9
8
  import com.amazonaws.services.s3.model.StorageClass.ReducedRedundancy
10
- import s3.website.S3.SuccessfulUpload
11
- import s3.website.S3.SuccessfulDelete
12
- import s3.website.S3.FailedUpload
13
- import scala.Some
14
- import s3.website.S3.FailedDelete
15
- import s3.website.S3.S3Setting
16
9
  import s3.website.ByteHelper.humanReadableByteCount
17
- import org.joda.time.{Seconds, Duration, Interval}
18
- import scala.concurrent.duration.TimeUnit
19
- import java.util.concurrent.TimeUnit
20
10
  import scala.concurrent.duration.TimeUnit
21
11
  import java.util.concurrent.TimeUnit.SECONDS
22
12
  import s3.website.S3.SuccessfulUpload.humanizeUploadSpeed
23
13
  import java.io.FileInputStream
24
14
  import s3.website.model.Config.awsCredentials
15
+ import scala.util.Try
25
16
 
26
17
  object S3 {
27
18
 
@@ -29,19 +20,26 @@ object S3 {
29
20
  (implicit config: Config, s3Settings: S3Setting, pushMode: PushMode, executor: ExecutionContextExecutor, logger: Logger) =
30
21
  upload(Right(redirect))
31
22
 
32
- def uploadFile(localFile: LocalFileFromDisk, a: Attempt = 1)
23
+ def uploadFile(localFile: LocalFile, a: Attempt = 1)
33
24
  (implicit config: Config, s3Settings: S3Setting, pushMode: PushMode, executor: ExecutionContextExecutor, logger: Logger) =
34
25
  upload(Left(localFile))
35
26
 
36
- def upload(source: Either[LocalFileFromDisk, Redirect], a: Attempt = 1)
27
+ def upload(source: Either[LocalFile, Redirect], a: Attempt = 1)
37
28
  (implicit config: Config, s3Settings: S3Setting, pushMode: PushMode, executor: ExecutionContextExecutor, logger: Logger):
38
29
  Future[Either[FailedUpload, SuccessfulUpload]] =
39
30
  Future {
40
- val putObjectRequest = toPutObjectRequest(source)
31
+ val putObjectRequest = toPutObjectRequest(source).get
41
32
  val uploadDuration =
42
33
  if (pushMode.dryRun) None
43
- else recordUploadDuration(putObjectRequest, s3Settings.s3Client(config) putObject putObjectRequest)
44
- val report = SuccessfulUpload(source, putObjectRequest, uploadDuration)
34
+ else Some(recordUploadDuration(putObjectRequest, s3Settings.s3Client(config) putObject putObjectRequest))
35
+ val report = SuccessfulUpload(
36
+ source.fold(_.s3Key, _.s3Key),
37
+ source.fold(
38
+ localFile => Left(SuccessfulNewOrCreatedDetails(localFile.uploadType, localFile.uploadFile.get.length(), uploadDuration)),
39
+ redirect => Right(SuccessfulRedirectDetails(redirect.uploadType, redirect.redirectTarget))
40
+ ),
41
+ putObjectRequest
42
+ )
45
43
  logger.info(report)
46
44
  Right(report)
47
45
  } recoverWith retry(a)(
@@ -62,25 +60,30 @@ object S3 {
62
60
  retryAction = newAttempt => this.delete(s3Key, newAttempt)
63
61
  )
64
62
 
65
- def toPutObjectRequest(source: Either[LocalFileFromDisk, Redirect])(implicit config: Config) =
63
+ def toPutObjectRequest(source: Either[LocalFile, Redirect])(implicit config: Config): Try[PutObjectRequest] =
66
64
  source.fold(
67
- localFile => {
68
- val md = new ObjectMetadata()
69
- md setContentLength localFile.uploadFile.length
70
- md setContentType localFile.contentType
71
- localFile.encodingOnS3.map(_ => "gzip") foreach md.setContentEncoding
72
- localFile.maxAge foreach { seconds =>
73
- md.setCacheControl(
74
- if (seconds == 0)
75
- s"no-cache; max-age=$seconds"
76
- else
77
- s"max-age=$seconds"
78
- )
65
+ localFile =>
66
+ for {
67
+ uploadFile <- localFile.uploadFile
68
+ contentType <- localFile.contentType
69
+ } yield {
70
+ val md = new ObjectMetadata()
71
+ md setContentLength uploadFile.length
72
+ md setContentType contentType
73
+ localFile.encodingOnS3.map(_ => "gzip") foreach md.setContentEncoding
74
+ localFile.maxAge foreach { seconds =>
75
+ md.setCacheControl(
76
+ if (seconds == 0)
77
+ s"no-cache; max-age=$seconds"
78
+ else
79
+ s"max-age=$seconds"
80
+ )
81
+ }
82
+ val req = new PutObjectRequest(config.s3_bucket, localFile.s3Key, new FileInputStream(uploadFile), md)
83
+ config.s3_reduced_redundancy.filter(_ == true) foreach (_ => req setStorageClass ReducedRedundancy)
84
+ req
79
85
  }
80
- val req = new PutObjectRequest(config.s3_bucket, localFile.s3Key, new FileInputStream(localFile.uploadFile), md)
81
- config.s3_reduced_redundancy.filter(_ == true) foreach (_ => req setStorageClass ReducedRedundancy)
82
- req
83
- },
86
+ ,
84
87
  redirect => {
85
88
  val req = new PutObjectRequest(config.s3_bucket, redirect.s3Key, redirect.redirectTarget)
86
89
  req.setMetadata({
@@ -93,17 +96,14 @@ object S3 {
93
96
  md.setCacheControl("max-age=0, no-cache")
94
97
  md
95
98
  })
96
- req
99
+ Try(req)
97
100
  }
98
101
  )
99
102
 
100
- def recordUploadDuration(putObjectRequest: PutObjectRequest, f: => Unit): Option[Duration] = {
103
+ def recordUploadDuration(putObjectRequest: PutObjectRequest, f: => Unit): UploadDuration = {
101
104
  val start = System.currentTimeMillis()
102
105
  f
103
- if (putObjectRequest.getMetadata.getContentLength > 0)
104
- Some(new Duration(start, System.currentTimeMillis))
105
- else
106
- None // We are not interested in tracking durations of PUT requests that don't contain data. Redirect is an example of such request.
106
+ System.currentTimeMillis - start
107
107
  }
108
108
 
109
109
  def awsS3Client(config: Config) = new AmazonS3Client(awsCredentials(config))
@@ -145,14 +145,18 @@ object S3 {
145
145
  def s3Key: String
146
146
  }
147
147
 
148
- case class SuccessfulUpload(source: Either[LocalFileFromDisk, Redirect], putObjectRequest: PutObjectRequest, uploadDuration: Option[Duration])
148
+ case class SuccessfulRedirectDetails(uploadType: UploadType, redirectTarget: String)
149
+ case class SuccessfulNewOrCreatedDetails(uploadType: UploadType, uploadSize: Long, uploadDuration: Option[Long])
150
+
151
+ case class SuccessfulUpload(s3Key: S3Key,
152
+ details: Either[SuccessfulNewOrCreatedDetails, SuccessfulRedirectDetails],
153
+ putObjectRequest: PutObjectRequest)
149
154
  (implicit pushMode: PushMode, logger: Logger) extends PushSuccessReport {
150
155
  def reportMessage =
151
- source.fold(_.uploadType, (redirect: Redirect) => redirect) match {
152
- case NewFile => s"${Created.renderVerb} $s3Key ($reportDetails)"
153
- case FileUpdate => s"${Updated.renderVerb} $s3Key ($reportDetails)"
154
- case Redirect(s3Key, redirectTarget) => s"${Redirected.renderVerb} $s3Key to $redirectTarget"
155
- }
156
+ details.fold(
157
+ newOrCreatedDetails => s"${newOrCreatedDetails.uploadType.pushAction} $s3Key ($reportDetails)",
158
+ redirectDetails => s"${redirectDetails.uploadType.pushAction} $s3Key to ${redirectDetails.redirectTarget}"
159
+ )
156
160
 
157
161
  def reportDetails = {
158
162
  val md = putObjectRequest.getMetadata
@@ -169,12 +173,9 @@ object S3 {
169
173
  }.mkString(" | ")
170
174
  }
171
175
 
172
- def s3Key = source.fold(_.s3Key, _.s3Key)
173
-
174
- lazy val uploadSize: Option[Long] =
175
- source.fold(
176
- (localFile: LocalFileFromDisk) => Some(localFile.uploadFile.length()),
177
- (redirect: Redirect) => None
176
+ lazy val uploadSize = details.fold(
177
+ newOrCreatedDetails => Some(newOrCreatedDetails.uploadSize),
178
+ redirectDetails => None
178
179
  )
179
180
 
180
181
  lazy val uploadSizeForHumans: Option[String] = uploadSize filter (_ => logger.verboseOutput) map humanReadableByteCount
@@ -182,17 +183,17 @@ object S3 {
182
183
  lazy val uploadSpeedForHumans: Option[String] =
183
184
  (for {
184
185
  dataSize <- uploadSize
185
- duration <- uploadDuration
186
+ duration <- details.left.map(_.uploadDuration).left.toOption.flatten
186
187
  } yield {
187
188
  humanizeUploadSpeed(dataSize, duration)
188
189
  }) flatMap identity filter (_ => logger.verboseOutput)
189
190
  }
190
191
 
191
192
  object SuccessfulUpload {
192
- def humanizeUploadSpeed(uploadedBytes: Long, uploadDurations: Duration*): Option[String] = {
193
- val totalDurationMillis = uploadDurations.foldLeft(new org.joda.time.Duration(0)){ (memo, duration) =>
194
- memo.plus(duration)
195
- }.getMillis // retain precision by using milliseconds
193
+ def humanizeUploadSpeed(uploadedBytes: Long, uploadDurations: UploadDuration*): Option[String] = {
194
+ val totalDurationMillis = uploadDurations.foldLeft(0L){ (memo, duration) =>
195
+ memo + duration
196
+ }
196
197
  if (totalDurationMillis > 0) {
197
198
  val bytesPerMillisecond = uploadedBytes / totalDurationMillis
198
199
  val bytesPerSecond = bytesPerMillisecond * 1000 * uploadDurations.length
@@ -48,7 +48,7 @@ object Site {
48
48
  redirects <- loadRedirects.right
49
49
  } yield {
50
50
  gzip_zopfli.foreach(_ => logger.info(
51
- """|zopfli is not currently supported
51
+ """|Zopfli is not currently supported. Falling back to regular gzip.
52
52
  |If you find a stable Java implementation for zopfli, please send an email to lauri.lehmijoki@iki.fi about it."""
53
53
  .stripMargin))
54
54
  extensionless_mime_type.foreach(_ => logger.info(
@@ -7,10 +7,11 @@ import java.util.zip.GZIPOutputStream
7
7
  import org.apache.tika.Tika
8
8
  import s3.website.Ruby._
9
9
  import s3.website._
10
- import s3.website.model.LocalFileFromDisk.tika
10
+ import s3.website.model.LocalFile.tika
11
11
  import s3.website.model.Encoding.encodingOnS3
12
12
  import java.io.File.createTempFile
13
13
  import org.apache.commons.io.IOUtils.copy
14
+ import scala.util.Try
14
15
 
15
16
  object Encoding {
16
17
 
@@ -34,27 +35,34 @@ object Encoding {
34
35
  }
35
36
  }
36
37
 
37
- sealed trait UploadType // Sealed, so that we can avoid inexhaustive pattern matches more easily
38
+ sealed trait UploadType {
39
+ val pushAction: PushAction
40
+ }
41
+
42
+ case object NewFile extends UploadType {
43
+ val pushAction = Created
44
+ }
45
+ case object FileUpdate extends UploadType {
46
+ val pushAction = Updated
47
+ }
38
48
 
39
- case object NewFile extends UploadType
40
- case object FileUpdate extends UploadType
41
- case object RedirectFile extends UploadType
49
+ case object RedirectFile extends UploadType {
50
+ val pushAction = Redirected
51
+ }
42
52
 
43
- case class LocalFileFromDisk(originalFile: File, uploadType: UploadType)(implicit site: Site) {
53
+ case class LocalFile(originalFile: File, uploadType: UploadType)(implicit site: Site) {
44
54
  lazy val s3Key = site.resolveS3Key(originalFile)
45
55
 
46
56
  lazy val encodingOnS3 = Encoding.encodingOnS3(s3Key)
47
57
 
48
- lazy val lastModified = originalFile.lastModified
49
-
50
58
  /**
51
59
  * This is the file we should upload, because it contains the potentially gzipped contents of the original file.
52
60
  *
53
61
  * May throw an exception, so remember to call this in a Try or Future monad
54
62
  */
55
- lazy val uploadFile: File = LocalFileFromDisk uploadFile originalFile
63
+ lazy val uploadFile: Try[File] = LocalFile uploadFile originalFile
56
64
 
57
- lazy val contentType = {
65
+ lazy val contentType: Try[String] = tika map { tika =>
58
66
  val mimeType = tika.detect(originalFile)
59
67
  if (mimeType.startsWith("text/") || mimeType == "application/json")
60
68
  mimeType + "; charset=utf-8"
@@ -84,24 +92,29 @@ case class LocalFileFromDisk(originalFile: File, uploadType: UploadType)(implici
84
92
  /**
85
93
  * May throw an exception, so remember to call this in a Try or Future monad
86
94
  */
87
- lazy val md5 = LocalFileFromDisk md5 originalFile
95
+ lazy val md5 = LocalFile md5 originalFile
88
96
  }
89
97
 
90
- object LocalFileFromDisk {
91
- lazy val tika = new Tika()
98
+ object LocalFile {
99
+ lazy val tika = Try(new Tika())
92
100
 
93
- def md5(originalFile: File)(implicit site: Site) = using(fis { uploadFile(originalFile) }) { DigestUtils.md5Hex }
101
+ def md5(originalFile: File)(implicit site: Site): Try[MD5] =
102
+ uploadFile(originalFile) map { file =>
103
+ using(fis { file }) { DigestUtils.md5Hex }
104
+ }
94
105
 
95
- def uploadFile(originalFile: File)(implicit site: Site): File =
106
+ def uploadFile(originalFile: File)(implicit site: Site): Try[File] =
96
107
  encodingOnS3(site resolveS3Key originalFile)
97
- .fold(originalFile)(algorithm => {
98
- val tempFile = createTempFile(originalFile.getName, "gzip")
99
- tempFile.deleteOnExit()
100
- using(new GZIPOutputStream(new FileOutputStream(tempFile))) { stream =>
101
- copy(fis(originalFile), stream)
108
+ .fold(Try(originalFile))(algorithm =>
109
+ Try {
110
+ val tempFile = createTempFile(originalFile.getName, "gzip")
111
+ tempFile.deleteOnExit()
112
+ using(new GZIPOutputStream(new FileOutputStream(tempFile))) { stream =>
113
+ copy(fis(originalFile), stream)
114
+ }
115
+ tempFile
102
116
  }
103
- tempFile
104
- })
117
+ )
105
118
 
106
119
  private[this] def fis(file: File): InputStream = new FileInputStream(file)
107
120
  private[this] def using[T <: Closeable, R](cl: T)(f: (T) => R): R = try f(cl) finally cl.close()
@@ -44,6 +44,8 @@ package object website {
44
44
 
45
45
  type S3Key = String
46
46
 
47
+ type UploadDuration = Long
48
+
47
49
  trait PushAction {
48
50
  def actionName = getClass.getSimpleName.replace("$", "") // case object class names contain the '$' char
49
51
 
@@ -21,13 +21,15 @@ import com.amazonaws.services.cloudfront.model.{CreateInvalidationResult, Create
21
21
  import org.mockito.stubbing.Answer
22
22
  import org.mockito.invocation.InvocationOnMock
23
23
  import java.util.concurrent.atomic.AtomicInteger
24
- import org.apache.commons.io.FileUtils.{forceDelete, forceMkdir, write}
24
+ import org.apache.commons.io.FileUtils._
25
25
  import scala.collection.mutable
26
26
  import s3.website.Push.{push, CliArgs}
27
27
  import s3.website.CloudFront.CloudFrontSetting
28
28
  import s3.website.S3.S3Setting
29
29
  import org.apache.commons.codec.digest.DigestUtils
30
30
  import java.util.Date
31
+ import s3.website.CloudFront.CloudFrontSetting
32
+ import s3.website.S3.S3Setting
31
33
 
32
34
  class S3WebsiteSpec extends Specification {
33
35
 
@@ -799,12 +801,15 @@ class S3WebsiteSpec extends Specification {
799
801
  trait EmptySite extends Directories {
800
802
  type LocalFileWithContent = (String, String)
801
803
 
802
- val localFilesWithContent: mutable.Set[LocalFileWithContent] = mutable.Set()
803
804
  def setLocalFile(fileName: String) = setLocalFileWithContent((fileName, ""))
804
805
  def setLocalFiles(fileNames: String*) = fileNames foreach setLocalFile
805
- def setLocalFileWithContent(fileNameAndContent: LocalFileWithContent) =
806
- localFilesWithContent += fileNameAndContent
807
806
  def setLocalFilesWithContent(fileNamesAndContent: LocalFileWithContent*) = fileNamesAndContent foreach setLocalFileWithContent
807
+ def setLocalFileWithContent(fileNameAndContent: LocalFileWithContent) = {
808
+ val file = new File(siteDirectory, fileNameAndContent._1)
809
+ forceMkdir(file.getParentFile)
810
+ file.createNewFile()
811
+ write(file, fileNameAndContent._2)
812
+ }
808
813
  var config = ""
809
814
  val baseConfig =
810
815
  """
@@ -813,21 +818,9 @@ class S3WebsiteSpec extends Specification {
813
818
  |s3_bucket: bucket
814
819
  """.stripMargin
815
820
 
816
- implicit def cliArgs: CliArgs = siteWithFilesAndContent(config, localFilesWithContent)
821
+ implicit def cliArgs: CliArgs = buildCliArgs(config)
817
822
  def pushMode: PushMode // Represents the --dry-run switch
818
823
 
819
- private def siteWithFilesAndContent(config: String = "", localFilesWithContent: mutable.Set[LocalFileWithContent]): CliArgs = {
820
- localFilesWithContent.foreach {
821
- case (filePath, content) =>
822
- val file = new File(siteDirectory, filePath)
823
- forceMkdir(file.getParentFile)
824
- file.createNewFile()
825
- write(file, content)
826
- }
827
- localFilesWithContent.clear() // Once we've persisted the changes on the disk, clear the queue. I.e., keep the state where it should be – on the disk.
828
- buildCliArgs(config)
829
- }
830
-
831
824
  private def buildCliArgs(
832
825
  config: String = "",
833
826
  baseConfig: String =
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: s3_website_monadic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.37
4
+ version: 0.0.38
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lauri Lehmijoki
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-05-29 00:00:00.000000000 Z
11
+ date: 2014-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor