s3_website_monadic 0.0.30 → 0.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a489b6afb931e8384aa6d656acd8bf23a0c366be
4
- data.tar.gz: 12234bf6f3689a83ad37a61c7029ce80900e2a67
3
+ metadata.gz: 240e4d2a14000e5eed029857d69e426cf7439c4f
4
+ data.tar.gz: ad0d71c36c1f0a8add0cca08f1de9f49467c2019
5
5
  SHA512:
6
- metadata.gz: bbc33e009ccbe805fe72697433d59fd4c82691cc9edd3e95d4e8277fceb159715f8b0d4566fa2c1e85d092c20cab2eb37178e0cfde0db1fc2103682f90653a67
7
- data.tar.gz: 6c8b915eab3220b1a7148d0912c267dfe7b05ac653367b033e83c9c6144d027d22d18ae2ec820314415b46433b0ec5335616a8f2cda730c46bab28577dbc8528
6
+ metadata.gz: 17edd14e1510e3b82604e6942a6bca4dc92b3bc34e6189fb4db5f62f2f930b32ea385794f19c1032d53f6d8c8d4600b5bd6cfc8f3ffd08f22f75e05b4b43544f
7
+ data.tar.gz: b3b818d8d18f40f36cf8b1661785da1fd38b8f4e9afeb78a33a37e39c5cd016bbd0824429330729e65fe01351506c044e51c9c80e372875fd4debebeaf83539f
data/build.sbt CHANGED
@@ -26,6 +26,10 @@ 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
+
29
33
  libraryDependencies += "org.specs2" %% "specs2" % "2.3.11" % "test"
30
34
 
31
35
  resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"
data/changelog.md CHANGED
@@ -10,6 +10,16 @@ This project uses [Semantic Versioning](http://semver.org).
10
10
 
11
11
  * Support CloudFront invalidations when the site contains over 3000 files
12
12
 
13
+ * Display transferred bytes
14
+
15
+ * Display upload speed
16
+
17
+ * `push --verbose` switch
18
+
19
+ ### Bug fixes
20
+
21
+ * Fault tolerance – do not crash if one of the uploads fails
22
+
13
23
  ### Java is now required
14
24
 
15
25
  * The `push` command is now written in Scala. This means that you need Java 1.6
@@ -1,3 +1,3 @@
1
1
  module S3Website
2
- VERSION = '0.0.30'
2
+ VERSION = '0.0.31'
3
3
  end
@@ -0,0 +1,14 @@
1
+ package s3.website;
2
+
3
+ public class ByteHelper {
4
+
5
+ // Adapted from http://stackoverflow.com/a/3758880/219947
6
+ public static String humanReadableByteCount(long bytes) {
7
+ boolean si = true;
8
+ int unit = si ? 1000 : 1024;
9
+ if (bytes < unit) return bytes + " B";
10
+ int exp = (int) (Math.log(bytes) / Math.log(unit));
11
+ String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i");
12
+ return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
13
+ }
14
+ }
@@ -121,7 +121,7 @@ object CloudFront {
121
121
 
122
122
 
123
123
  def needsInvalidation: PartialFunction[PushSuccessReport, Boolean] = {
124
- case SuccessfulUpload(upload, _) => upload.uploadType match {
124
+ case SuccessfulUpload(upload, _, _) => upload.uploadType match {
125
125
  case Update => true
126
126
  case _ => false
127
127
  }
@@ -6,13 +6,15 @@ import s3.website._
6
6
 
7
7
  object Diff {
8
8
 
9
- def resolveDeletes(localFiles: Seq[LocalFile], s3Files: Seq[S3File], redirects: Seq[Upload with UploadTypeResolved])(implicit config: Config): Seq[S3File] = {
9
+ def resolveDeletes(localFiles: Seq[LocalFile], s3Files: Seq[S3File], redirects: Seq[Upload with UploadTypeResolved])
10
+ (implicit config: Config, logger: Logger): Seq[S3File] = {
10
11
  val keysNotToBeDeleted: Set[String] = (localFiles ++ redirects).map(_.s3Key).toSet
11
12
  s3Files.filterNot { s3File =>
12
13
  val ignoreOnServer = config.ignore_on_server.exists(_.fold(
13
14
  (ignoreRegex: String) => rubyRegexMatches(s3File.s3Key, ignoreRegex),
14
15
  (ignoreRegexes: Seq[String]) => ignoreRegexes.exists(rubyRegexMatches(s3File.s3Key, _))
15
16
  ))
17
+ if (ignoreOnServer) logger.debug(s"Ignoring ${s3File.s3Key} on server")
16
18
  keysNotToBeDeleted.exists(_ == s3File.s3Key) || ignoreOnServer
17
19
  }
18
20
  }
@@ -3,7 +3,6 @@ package s3.website
3
3
  import s3.website.model.Site._
4
4
  import scala.concurrent.{ExecutionContextExecutor, Future, Await}
5
5
  import scala.concurrent.duration._
6
- import com.lexicalscope.jewel.cli.CliFactory
7
6
  import scala.language.postfixOps
8
7
  import s3.website.Diff.{resolveNewFiles, resolveDeletes}
9
8
  import s3.website.S3._
@@ -27,9 +26,54 @@ import s3.website.CloudFront.FailedInvalidation
27
26
  import scala.Int
28
27
  import java.io.File
29
28
  import com.lexicalscope.jewel.cli.CliFactory.parseArguments
29
+ import s3.website.ByteHelper.humanReadableByteCount
30
30
 
31
31
  object Push {
32
32
 
33
+ def main(args: Array[String]) {
34
+ implicit val cliArgs = parseArguments(classOf[CliArgs], args:_*)
35
+ implicit val s3Settings = S3Setting()
36
+ implicit val cloudFrontSettings = CloudFrontSetting()
37
+ implicit val workingDirectory = new File(System.getProperty("user.dir")).getAbsoluteFile
38
+ System exit push
39
+ }
40
+
41
+ trait CliArgs {
42
+ import com.lexicalscope.jewel.cli.Option
43
+
44
+ @Option(defaultToNull = true) def site: String
45
+ @Option(longName = Array("config-dir"), defaultToNull = true) def configDir: String
46
+ @Option def verbose: Boolean
47
+ @Option(longName = Array("dry-run")) def dryRun: Boolean
48
+ }
49
+
50
+ def push(implicit cliArgs: CliArgs, s3Settings: S3Setting, cloudFrontSettings: CloudFrontSetting, workingDirectory: File): ExitCode = {
51
+ implicit val logger: Logger = new Logger(cliArgs.verbose)
52
+ implicit val pushMode = new PushMode {
53
+ def dryRun = cliArgs.dryRun
54
+ }
55
+
56
+ val errorOrSiteDir: ErrorOrFile =
57
+ Option(cliArgs.site).fold(Ssg.findSiteDirectory(workingDirectory))(siteDirFromCli => Right(new File(siteDirFromCli)))
58
+ def errorOrSite(siteInDirectory: File): Either[ErrorReport, Site] =
59
+ loadSite(Option(cliArgs.configDir).getOrElse(workingDirectory.getPath) + "/s3_website.yml", siteInDirectory.getAbsolutePath)
60
+
61
+ val errorOrPushStatus = for {
62
+ siteInDirectory <- errorOrSiteDir.right
63
+ loadedSite <- errorOrSite(siteInDirectory).right
64
+ } yield {
65
+ implicit val site = loadedSite
66
+ val threadPool = newFixedThreadPool(site.config.concurrency_level)
67
+ implicit val executor = fromExecutor(threadPool)
68
+ val pushStatus = pushSite
69
+ threadPool.shutdownNow()
70
+ pushStatus
71
+ }
72
+
73
+ errorOrPushStatus.left foreach (err => logger.fail(s"Could not load the site: ${err.reportMessage}"))
74
+ errorOrPushStatus fold((err: ErrorReport) => 1, pushStatus => pushStatus)
75
+ }
76
+
33
77
  def pushSite(
34
78
  implicit site: Site,
35
79
  executor: ExecutionContextExecutor,
@@ -139,34 +183,46 @@ object Push {
139
183
  })
140
184
 
141
185
  def resolvePushCounts(implicit finishedOperations: FinishedPushOperations) = finishedOperations.foldLeft(PushCounts()) {
142
- (counts: PushCounts, uploadReport) => uploadReport.fold(
143
- (error: ErrorReport) => counts.copy(failures = counts.failures + 1),
144
- failureOrSuccess => failureOrSuccess.fold(
145
- (failureReport: PushFailureReport) => counts.copy(failures = counts.failures + 1),
146
- (successReport: PushSuccessReport) => successReport match {
147
- case SuccessfulUpload(upload, _) => upload.uploadType match {
148
- case NewFile => counts.copy(newFiles = counts.newFiles + 1)
149
- case Update => counts.copy(updates = counts.updates + 1)
150
- case Redirect => counts.copy(redirects = counts.redirects + 1)
186
+ (counts: PushCounts, uploadReport) =>
187
+ uploadReport.fold(
188
+ (error: ErrorReport) => counts.copy(failures = counts.failures + 1),
189
+ failureOrSuccess => failureOrSuccess.fold(
190
+ (failureReport: PushFailureReport) => counts.copy(failures = counts.failures + 1),
191
+ (successReport: PushSuccessReport) => successReport match {
192
+ case succ: SuccessfulUpload => succ.upload.uploadType match {
193
+ case NewFile => counts.copy(newFiles = counts.newFiles + 1).addTransferStats(succ) // TODO nasty repetition here
194
+ case Update => counts.copy(updates = counts.updates + 1).addTransferStats(succ)
195
+ case Redirect => counts.copy(redirects = counts.redirects + 1).addTransferStats(succ)
196
+ }
197
+ case SuccessfulDelete(_) => counts.copy(deletes = counts.deletes + 1)
151
198
  }
152
- case SuccessfulDelete(_) => counts.copy(deletes = counts.deletes + 1)
153
- }
199
+ )
154
200
  )
155
- )
156
201
  }
157
202
 
158
203
  def pushCountsToString(pushCounts: PushCounts)(implicit pushMode: PushMode): String =
159
204
  pushCounts match {
160
- case PushCounts(updates, newFiles, failures, redirects, deletes)
205
+ case PushCounts(updates, newFiles, failures, redirects, deletes, _, _)
161
206
  if updates == 0 && newFiles == 0 && failures == 0 && redirects == 0 && deletes == 0 =>
162
207
  PushNothing.renderVerb
163
- case PushCounts(updates, newFiles, failures, redirects, deletes) =>
208
+ case PushCounts(updates, newFiles, failures, redirects, deletes, uploadedBytes, uploadDurationAndFrequency) =>
164
209
  val reportClauses: scala.collection.mutable.ArrayBuffer[String] = ArrayBuffer()
165
- if (updates > 0) reportClauses += s"${Updated.renderVerb} ${updates ofType "file"}."
166
- if (newFiles > 0) reportClauses += s"${Created.renderVerb} ${newFiles ofType "file"}."
167
- if (failures > 0) reportClauses += s"${failures ofType "operation"} failed." // This includes both failed uploads and deletes.
168
- if (redirects > 0) reportClauses += s"${Applied.renderVerb} ${redirects ofType "redirect"}."
169
- if (deletes > 0) reportClauses += s"${Deleted.renderVerb} ${deletes ofType "file"}."
210
+ if (updates > 0) reportClauses += s"${Updated.renderVerb} ${updates ofType "file"}."
211
+ if (newFiles > 0) reportClauses += s"${Created.renderVerb} ${newFiles ofType "file"}."
212
+ if (failures > 0) reportClauses += s"${failures ofType "operation"} failed." // This includes both failed uploads and deletes.
213
+ if (redirects > 0) reportClauses += s"${Applied.renderVerb} ${redirects ofType "redirect"}."
214
+ if (deletes > 0) reportClauses += s"${Deleted.renderVerb} ${deletes ofType "file"}."
215
+ if (uploadedBytes > 0) {
216
+ val transferSuffix =
217
+ if (uploadDurationAndFrequency._1.getStandardSeconds > 0)
218
+ s", ${humanReadableByteCount(
219
+ (uploadedBytes / uploadDurationAndFrequency._1.getMillis * 1000) * uploadDurationAndFrequency._2
220
+ )}/s."
221
+ else
222
+ "."
223
+
224
+ reportClauses += s"${Transferred.renderVerb} ${humanReadableByteCount(uploadedBytes)}$transferSuffix"
225
+ }
170
226
  reportClauses.mkString(" ")
171
227
  }
172
228
 
@@ -175,56 +231,24 @@ object Push {
175
231
  newFiles: Int = 0,
176
232
  failures: Int = 0,
177
233
  redirects: Int = 0,
178
- deletes: Int = 0
234
+ deletes: Int = 0,
235
+ uploadedBytes: Long = 0,
236
+ uploadDurationAndFrequency: (org.joda.time.Duration, Int) = (new org.joda.time.Duration(0), 0)
179
237
  ) {
180
238
  val thereWasSomethingToPush = updates + newFiles + redirects + deletes > 0
239
+
240
+ def addTransferStats(successfulUpload: SuccessfulUpload): PushCounts = {
241
+ copy(
242
+ uploadedBytes = uploadedBytes + (successfulUpload.uploadSize getOrElse 0L),
243
+ uploadDurationAndFrequency = successfulUpload.uploadDuration.fold(uploadDurationAndFrequency)(
244
+ dur => (uploadDurationAndFrequency._1.plus(dur), uploadDurationAndFrequency._2 + 1)
245
+ )
246
+ )
247
+ }
181
248
  }
249
+
182
250
  type FinishedPushOperations = ParSeq[Either[ErrorReport, PushErrorOrSuccess]]
183
251
  type PushReports = ParSeq[Either[ErrorReport, Future[PushErrorOrSuccess]]]
184
252
  case class PushResult(threadPool: ExecutorService, uploadReports: PushReports)
185
253
  type ExitCode = Int
186
-
187
- trait CliArgs {
188
- import com.lexicalscope.jewel.cli.Option
189
-
190
- @Option(defaultToNull = true) def site: String
191
- @Option(longName = Array("config-dir"), defaultToNull = true) def configDir: String
192
- @Option def verbose: Boolean
193
- @Option(longName = Array("dry-run")) def dryRun: Boolean
194
- }
195
-
196
- def main(args: Array[String]) {
197
- implicit val cliArgs = parseArguments(classOf[CliArgs], args:_*)
198
- implicit val s3Settings = S3Setting()
199
- implicit val cloudFrontSettings = CloudFrontSetting()
200
- implicit val workingDirectory = new File(System.getProperty("user.dir")).getAbsoluteFile
201
- System exit push
202
- }
203
-
204
- def push(implicit cliArgs: CliArgs, s3Settings: S3Setting, cloudFrontSettings: CloudFrontSetting, workingDirectory: File): ExitCode = {
205
- implicit val logger: Logger = new Logger(cliArgs.verbose)
206
- implicit val pushMode = new PushMode {
207
- def dryRun = cliArgs.dryRun
208
- }
209
-
210
- val errorOrSiteDir: ErrorOrFile =
211
- Option(cliArgs.site).fold(Ssg.findSiteDirectory(workingDirectory))(siteDirFromCli => Right(new File(siteDirFromCli)))
212
- def errorOrSite(siteInDirectory: File): Either[ErrorReport, Site] =
213
- loadSite(Option(cliArgs.configDir).getOrElse(workingDirectory.getPath) + "/s3_website.yml", siteInDirectory.getAbsolutePath)
214
-
215
- val errorOrPushStatus = for {
216
- siteInDirectory <- errorOrSiteDir.right
217
- loadedSite <- errorOrSite(siteInDirectory).right
218
- } yield {
219
- implicit val site = loadedSite
220
- val threadPool = newFixedThreadPool(site.config.concurrency_level)
221
- implicit val executor = fromExecutor(threadPool)
222
- val pushStatus = pushSite
223
- threadPool.shutdownNow()
224
- pushStatus
225
- }
226
-
227
- errorOrPushStatus.left foreach (err => logger.fail(s"Could not load the site: ${err.reportMessage}"))
228
- errorOrPushStatus fold((err: ErrorReport) => 1, pushStatus => pushStatus)
229
- }
230
254
  }
@@ -7,23 +7,29 @@ import com.amazonaws.services.s3.model._
7
7
  import scala.collection.JavaConversions._
8
8
  import scala.concurrent.{ExecutionContextExecutor, Future}
9
9
  import com.amazonaws.services.s3.model.StorageClass.ReducedRedundancy
10
- import scala.concurrent.duration.TimeUnit
11
- import java.util.concurrent.TimeUnit.SECONDS
12
10
  import s3.website.S3.SuccessfulUpload
13
11
  import s3.website.S3.SuccessfulDelete
14
12
  import s3.website.S3.FailedUpload
15
13
  import scala.Some
16
14
  import s3.website.S3.FailedDelete
17
15
  import s3.website.S3.S3Setting
16
+ 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
+ import scala.concurrent.duration.TimeUnit
21
+ import java.util.concurrent.TimeUnit.SECONDS
18
22
 
19
- class S3(implicit s3Settings: S3Setting, pushMode: PushMode, executor: ExecutionContextExecutor) {
23
+ class S3(implicit s3Settings: S3Setting, pushMode: PushMode, executor: ExecutionContextExecutor, logger: Logger) {
20
24
 
21
25
  def upload(upload: Upload with UploadTypeResolved, a: Attempt = 1)
22
- (implicit config: Config, logger: Logger): Future[Either[FailedUpload, SuccessfulUpload]] =
26
+ (implicit config: Config): Future[Either[FailedUpload, SuccessfulUpload]] =
23
27
  Future {
24
28
  val putObjectRequest = toPutObjectRequest(upload)
25
- if (!pushMode.dryRun) s3Settings.s3Client(config) putObject putObjectRequest
26
- val report = SuccessfulUpload(upload, putObjectRequest)
29
+ val uploadDuration =
30
+ if (pushMode.dryRun) None
31
+ else recordUploadDuration(putObjectRequest, s3Settings.s3Client(config) putObject putObjectRequest)
32
+ val report = SuccessfulUpload(upload, putObjectRequest, uploadDuration)
27
33
  logger.info(report)
28
34
  Right(report)
29
35
  } recoverWith retry(a)(
@@ -32,7 +38,7 @@ class S3(implicit s3Settings: S3Setting, pushMode: PushMode, executor: Execution
32
38
  )
33
39
 
34
40
  def delete(s3Key: String, a: Attempt = 1)
35
- (implicit config: Config, logger: Logger): Future[Either[FailedDelete, SuccessfulDelete]] =
41
+ (implicit config: Config): Future[Either[FailedDelete, SuccessfulDelete]] =
36
42
  Future {
37
43
  if (!pushMode.dryRun) s3Settings.s3Client(config) deleteObject(config.s3_bucket, s3Key)
38
44
  val report = SuccessfulDelete(s3Key)
@@ -42,7 +48,7 @@ class S3(implicit s3Settings: S3Setting, pushMode: PushMode, executor: Execution
42
48
  createFailureReport = error => FailedDelete(s3Key, error),
43
49
  retryAction = newAttempt => this.delete(s3Key, newAttempt)
44
50
  )
45
-
51
+
46
52
  def toPutObjectRequest(upload: Upload)(implicit config: Config) =
47
53
  upload.essence.fold(
48
54
  redirect => {
@@ -77,6 +83,15 @@ class S3(implicit s3Settings: S3Setting, pushMode: PushMode, executor: Execution
77
83
  req
78
84
  }
79
85
  )
86
+
87
+ def recordUploadDuration(putObjectRequest: PutObjectRequest, f: => Unit): Option[Duration] = {
88
+ val start = System.currentTimeMillis()
89
+ f
90
+ if (putObjectRequest.getMetadata.getContentLength > 0)
91
+ Some(new Duration(start, System.currentTimeMillis))
92
+ else
93
+ None // We are not interested in tracking durations of PUT requests that don't contain data. Redirect is an example of such request.
94
+ }
80
95
  }
81
96
 
82
97
  object S3 {
@@ -138,21 +153,47 @@ object S3 {
138
153
  def s3Key: String
139
154
  }
140
155
 
141
- case class SuccessfulUpload(upload: Upload with UploadTypeResolved, putObjectRequest: PutObjectRequest)
142
- (implicit pushMode: PushMode) extends PushSuccessReport {
143
- val metadata = putObjectRequest.getMetadata
144
- def metadataReport =
145
- (metadata.getCacheControl :: metadata.getContentType :: metadata.getContentEncoding :: putObjectRequest.getStorageClass :: Nil)
146
- .filterNot(_ == null)
147
- .mkString(" | ")
148
-
156
+ case class SuccessfulUpload(upload: Upload with UploadTypeResolved, putObjectRequest: PutObjectRequest, uploadDuration: Option[Duration])
157
+ (implicit pushMode: PushMode, logger: Logger) extends PushSuccessReport {
149
158
  def reportMessage =
150
159
  upload.uploadType match {
151
- case NewFile => s"${Created.renderVerb} ${upload.s3Key} ($metadataReport)"
152
- case Update => s"${Updated.renderVerb} ${upload.s3Key} ($metadataReport)"
160
+ case NewFile => s"${Created.renderVerb} $s3Key ($reportDetails)"
161
+ case Update => s"${Updated.renderVerb} $s3Key ($reportDetails)"
153
162
  case Redirect => s"${Redirected.renderVerb} ${upload.essence.left.get.key} to ${upload.essence.left.get.redirectTarget}"
154
163
  }
155
164
 
165
+ def reportDetails = {
166
+ val md = putObjectRequest.getMetadata
167
+ val detailFragments: Seq[Option[String]] =
168
+ (
169
+ md.getCacheControl ::
170
+ md.getContentType ::
171
+ md.getContentEncoding ::
172
+ putObjectRequest.getStorageClass ::
173
+ Nil map (Option(_)) // AWS SDK may return nulls
174
+ ) :+ uploadSizeForHumans :+ uploadSpeedForHumans
175
+ detailFragments.collect {
176
+ case Some(detailFragment) => detailFragment
177
+ }.mkString(" | ")
178
+ }
179
+
180
+ lazy val uploadSize: Option[Long] =
181
+ upload.essence.fold(
182
+ (redirect: Redirect) => None,
183
+ uploadBody => Some(uploadBody.contentLength)
184
+ )
185
+
186
+ lazy val uploadSizeForHumans: Option[String] = uploadSize filter (_ => logger.verboseOutput) map humanReadableByteCount
187
+
188
+ lazy val uploadSpeed: Option[Long] = for {
189
+ dataSize <- uploadSize
190
+ duration <- uploadDuration
191
+ } yield (dataSize / (duration.getMillis max 1)) * 1000 // Precision tweaking and avoidance of divide-by-zero
192
+
193
+ lazy val uploadSpeedForHumans: Option[String] = uploadSpeed filter (_ => logger.verboseOutput) map {
194
+ bytesPerSecond => s"${humanReadableByteCount(bytesPerSecond)}/s"
195
+ }
196
+
156
197
  def s3Key = upload.s3Key
157
198
  }
158
199
 
@@ -16,7 +16,7 @@ object Utils {
16
16
  lazy val fibs: Stream[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map { n => n._1 + n._2 }
17
17
  }
18
18
 
19
- class Logger(verboseOutput: Boolean, logMessage: (String) => Unit = println) {
19
+ class Logger(val verboseOutput: Boolean, logMessage: (String) => Unit = println) {
20
20
  import Rainbow._
21
21
  def debug(msg: String) = if (verboseOutput) log(Debug, msg)
22
22
  def info(msg: String) = log(Info, msg)
@@ -137,17 +137,19 @@ object LocalFile {
137
137
  }
138
138
  }
139
139
 
140
- def resolveLocalFiles(implicit site: Site): Either[ErrorReport, Seq[LocalFile]] = Try {
140
+ def resolveLocalFiles(implicit site: Site, logger: Logger): Either[ErrorReport, Seq[LocalFile]] = Try {
141
141
  val files = recursiveListFiles(new File(site.rootDirectory)).filterNot(_.isDirectory)
142
142
  files map { file =>
143
143
  val s3Key = site.resolveS3Key(file)
144
144
  LocalFile(s3Key, file, encodingOnS3(s3Key))
145
145
  } filterNot { file =>
146
- site.config.exclude_from_upload exists { _.fold(
146
+ val excludeFile = site.config.exclude_from_upload exists { _.fold(
147
147
  // For backward compatibility, use Ruby regex matching
148
148
  (exclusionRegex: String) => rubyRegexMatches(file.s3Key, exclusionRegex),
149
149
  (exclusionRegexes: Seq[String]) => exclusionRegexes exists (rubyRegexMatches(file.s3Key, _))
150
150
  ) }
151
+ if (excludeFile) logger.debug(s"Excluded ${file.s3Key} from upload")
152
+ excludeFile
151
153
  } filterNot { _.originalFile.getName == "s3_website.yml" } // For security reasons, the s3_website.yml should never be pushed
152
154
  } match {
153
155
  case Success(localFiles) =>
@@ -39,6 +39,7 @@ package object website {
39
39
  case object Updated extends PushAction
40
40
  case object Redirected extends PushAction
41
41
  case object Deleted extends PushAction
42
+ case object Transferred extends PushAction
42
43
  case object Invalidated extends PushAction
43
44
  case object Applied extends PushAction
44
45
  case object PushNothing extends PushAction {
@@ -23,7 +23,7 @@ import org.mockito.invocation.InvocationOnMock
23
23
  import java.util.concurrent.atomic.AtomicInteger
24
24
  import org.apache.commons.io.FileUtils.{forceDelete, forceMkdir, write}
25
25
  import scala.collection.mutable
26
- import s3.website.Push.CliArgs
26
+ import s3.website.Push.{push, CliArgs}
27
27
 
28
28
  class S3WebsiteSpec extends Specification {
29
29
 
@@ -32,7 +32,7 @@ class S3WebsiteSpec extends Specification {
32
32
  config = "gzip: true"
33
33
  setLocalFileWithContent(("styles.css", "<h1>hi again</h1>"))
34
34
  setS3Files(S3File("styles.css", "1c5117e5839ad8fc00ce3c41296255a1" /* md5 of the gzip of the file contents */))
35
- Push.push
35
+ push
36
36
  sentPutObjectRequest.getKey must equalTo("styles.css")
37
37
  }
38
38
 
@@ -40,7 +40,7 @@ class S3WebsiteSpec extends Specification {
40
40
  config = "gzip: true"
41
41
  setLocalFileWithContent(("styles.css", "<h1>hi</h1>"))
42
42
  setS3Files(S3File("styles.css", "1c5117e5839ad8fc00ce3c41296255a1" /* md5 of the gzip of the file contents */))
43
- Push.push
43
+ push
44
44
  noUploadsOccurred must beTrue
45
45
  }
46
46
  }
@@ -56,7 +56,7 @@ class S3WebsiteSpec extends Specification {
56
56
  """.stripMargin
57
57
  setLocalFileWithContent(("file.xml", "<h1>hi again</h1>"))
58
58
  setS3Files(S3File("file.xml", "1c5117e5839ad8fc00ce3c41296255a1" /* md5 of the gzip of the file contents */))
59
- Push.push
59
+ push
60
60
  sentPutObjectRequest.getKey must equalTo("file.xml")
61
61
  }
62
62
  }
@@ -65,33 +65,33 @@ class S3WebsiteSpec extends Specification {
65
65
  "not upload a file if it has not changed" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
66
66
  setLocalFileWithContent(("index.html", "<div>hello</div>"))
67
67
  setS3Files(S3File("index.html", md5Hex("<div>hello</div>")))
68
- Push.push
68
+ push
69
69
  noUploadsOccurred must beTrue
70
70
  }
71
71
 
72
72
  "update a file if it has changed" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
73
73
  setLocalFileWithContent(("index.html", "<h1>old text</h1>"))
74
74
  setS3Files(S3File("index.html", md5Hex("<h1>new text</h1>")))
75
- Push.push
75
+ push
76
76
  sentPutObjectRequest.getKey must equalTo("index.html")
77
77
  }
78
78
 
79
79
  "create a file if does not exist on S3" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
80
80
  setLocalFile("index.html")
81
- Push.push
81
+ push
82
82
  sentPutObjectRequest.getKey must equalTo("index.html")
83
83
  }
84
84
 
85
85
  "delete files that are on S3 but not on local file system" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
86
86
  setS3Files(S3File("old.html", md5Hex("<h1>old text</h1>")))
87
- Push.push
87
+ push
88
88
  sentDelete must equalTo("old.html")
89
89
  }
90
90
 
91
91
  "try again if the upload fails" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
92
92
  setLocalFile("index.html")
93
93
  uploadFailsAndThenSucceeds(howManyFailures = 5)
94
- Push.push
94
+ push
95
95
  verify(amazonS3Client, times(6)).putObject(Matchers.any(classOf[PutObjectRequest]))
96
96
  }
97
97
 
@@ -102,21 +102,21 @@ class S3WebsiteSpec extends Specification {
102
102
  e.setStatusCode(403)
103
103
  e
104
104
  }
105
- Push.push
105
+ push
106
106
  verify(amazonS3Client, times(1)).putObject(Matchers.any(classOf[PutObjectRequest]))
107
107
  }
108
108
 
109
109
  "try again if the delete fails" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
110
110
  setS3Files(S3File("old.html", md5Hex("<h1>old text</h1>")))
111
111
  deleteFailsAndThenSucceeds(howManyFailures = 5)
112
- Push.push
112
+ push
113
113
  verify(amazonS3Client, times(6)).deleteObject(Matchers.anyString(), Matchers.anyString())
114
114
  }
115
115
 
116
116
  "try again if the object listing fails" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
117
117
  setS3Files(S3File("old.html", md5Hex("<h1>old text</h1>")))
118
118
  objectListingFailsAndThenSucceeds(howManyFailures = 5)
119
- Push.push
119
+ push
120
120
  verify(amazonS3Client, times(6)).listObjects(Matchers.any(classOf[ListObjectsRequest]))
121
121
  }
122
122
  }
@@ -126,14 +126,14 @@ class S3WebsiteSpec extends Specification {
126
126
  config = "cloudfront_distribution_id: EGM1J2JJX9Z"
127
127
  setLocalFiles("css/test.css", "articles/index.html")
128
128
  setOutdatedS3Keys("css/test.css", "articles/index.html")
129
- Push.push
129
+ push
130
130
  sentInvalidationRequest.getInvalidationBatch.getPaths.getItems.toSeq.sorted must equalTo(("/css/test.css" :: "/articles/index.html" :: Nil).sorted)
131
131
  }
132
132
 
133
133
  "not send CloudFront invalidation requests on new objects" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
134
134
  config = "cloudfront_distribution_id: EGM1J2JJX9Z"
135
135
  setLocalFile("newfile.js")
136
- Push.push
136
+ push
137
137
  noInvalidationsOccurred must beTrue
138
138
  }
139
139
 
@@ -143,7 +143,7 @@ class S3WebsiteSpec extends Specification {
143
143
  |redirects:
144
144
  | /index.php: index.html
145
145
  """.stripMargin
146
- Push.push
146
+ push
147
147
  noInvalidationsOccurred must beTrue
148
148
  }
149
149
 
@@ -152,7 +152,7 @@ class S3WebsiteSpec extends Specification {
152
152
  config = "cloudfront_distribution_id: EGM1J2JJX9Z"
153
153
  setLocalFile("test.css")
154
154
  setOutdatedS3Keys("test.css")
155
- Push.push must equalTo(0) // The retries should finally result in a success
155
+ push must equalTo(0) // The retries should finally result in a success
156
156
  sentInvalidationRequests.length must equalTo(4)
157
157
  }
158
158
 
@@ -161,7 +161,7 @@ class S3WebsiteSpec extends Specification {
161
161
  config = "cloudfront_distribution_id: EGM1J2JJX9Z"
162
162
  setLocalFile("test.css")
163
163
  setOutdatedS3Keys("test.css")
164
- Push.push
164
+ push
165
165
  sentInvalidationRequests.length must equalTo(6)
166
166
  }
167
167
 
@@ -169,7 +169,7 @@ class S3WebsiteSpec extends Specification {
169
169
  config = "cloudfront_distribution_id: EGM1J2JJX9Z"
170
170
  setLocalFile("articles/arnold's file.html")
171
171
  setOutdatedS3Keys("articles/arnold's file.html")
172
- Push.push
172
+ push
173
173
  sentInvalidationRequest.getInvalidationBatch.getPaths.getItems.toSeq.sorted must equalTo(("/articles/arnold's%20file.html" :: Nil).sorted)
174
174
  }
175
175
 
@@ -177,7 +177,7 @@ class S3WebsiteSpec extends Specification {
177
177
  config = "cloudfront_distribution_id: EGM1J2JJX9Z"
178
178
  setLocalFile("maybe-index.html")
179
179
  setOutdatedS3Keys("maybe-index.html")
180
- Push.push
180
+ push
181
181
  sentInvalidationRequest.getInvalidationBatch.getPaths.getItems.toSeq.sorted must equalTo(("/" :: "/maybe-index.html" :: Nil).sorted)
182
182
  }
183
183
  }
@@ -190,7 +190,7 @@ class S3WebsiteSpec extends Specification {
190
190
  """.stripMargin
191
191
  setLocalFile("articles/index.html")
192
192
  setOutdatedS3Keys("articles/index.html")
193
- Push.push
193
+ push
194
194
  sentInvalidationRequest.getInvalidationBatch.getPaths.getItems.toSeq.sorted must equalTo(("/articles/" :: Nil).sorted)
195
195
  }
196
196
  }
@@ -201,7 +201,7 @@ class S3WebsiteSpec extends Specification {
201
201
  config = "cloudfront_distribution_id: EGM1J2JJX9Z"
202
202
  setLocalFiles(files:_*)
203
203
  setOutdatedS3Keys(files:_*)
204
- Push.push
204
+ push
205
205
  sentInvalidationRequests.length must equalTo(2)
206
206
  sentInvalidationRequests(0).getInvalidationBatch.getPaths.getItems.length must equalTo(1000)
207
207
  sentInvalidationRequests(1).getInvalidationBatch.getPaths.getItems.length must equalTo(2)
@@ -211,13 +211,13 @@ class S3WebsiteSpec extends Specification {
211
211
  "push exit status" should {
212
212
  "be 0 all uploads succeed" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
213
213
  setLocalFiles("file.txt")
214
- Push.push must equalTo(0)
214
+ push must equalTo(0)
215
215
  }
216
216
 
217
217
  "be 1 if any of the uploads fails" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
218
218
  setLocalFiles("file.txt")
219
219
  when(amazonS3Client.putObject(Matchers.any(classOf[PutObjectRequest]))).thenThrow(new AmazonServiceException("AWS failed"))
220
- Push.push must equalTo(1)
220
+ push must equalTo(1)
221
221
  }
222
222
 
223
223
  "be 1 if any of the redirects fails" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
@@ -226,14 +226,14 @@ class S3WebsiteSpec extends Specification {
226
226
  | index.php: /index.html
227
227
  """.stripMargin
228
228
  when(amazonS3Client.putObject(Matchers.any(classOf[PutObjectRequest]))).thenThrow(new AmazonServiceException("AWS failed"))
229
- Push.push must equalTo(1)
229
+ push must equalTo(1)
230
230
  }
231
231
 
232
232
  "be 0 if CloudFront invalidations and uploads succeed"in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
233
233
  config = "cloudfront_distribution_id: EGM1J2JJX9Z"
234
234
  setLocalFile("test.css")
235
235
  setOutdatedS3Keys("test.css")
236
- Push.push must equalTo(0)
236
+ push must equalTo(0)
237
237
  }
238
238
 
239
239
  "be 1 if CloudFront is unreachable or broken"in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
@@ -241,32 +241,32 @@ class S3WebsiteSpec extends Specification {
241
241
  config = "cloudfront_distribution_id: EGM1J2JJX9Z"
242
242
  setLocalFile("test.css")
243
243
  setOutdatedS3Keys("test.css")
244
- Push.push must equalTo(1)
244
+ push must equalTo(1)
245
245
  }
246
246
 
247
247
  "be 0 if upload retry succeeds" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
248
248
  setLocalFile("index.html")
249
249
  uploadFailsAndThenSucceeds(howManyFailures = 1)
250
- Push.push must equalTo(0)
250
+ push must equalTo(0)
251
251
  }
252
252
 
253
253
  "be 1 if delete retry fails" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
254
254
  setLocalFile("index.html")
255
255
  uploadFailsAndThenSucceeds(howManyFailures = 6)
256
- Push.push must equalTo(1)
256
+ push must equalTo(1)
257
257
  }
258
258
 
259
259
  "be 1 if an object listing fails" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
260
260
  setS3Files(S3File("old.html", md5Hex("<h1>old text</h1>")))
261
261
  objectListingFailsAndThenSucceeds(howManyFailures = 6)
262
- Push.push must equalTo(1)
262
+ push must equalTo(1)
263
263
  }
264
264
  }
265
265
 
266
266
  "s3_website.yml file" should {
267
267
  "never be uploaded" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
268
268
  setLocalFile("s3_website.yml")
269
- Push.push
269
+ push
270
270
  noUploadsOccurred must beTrue
271
271
  }
272
272
  }
@@ -275,7 +275,7 @@ class S3WebsiteSpec extends Specification {
275
275
  "result in matching files not being uploaded" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
276
276
  config = "exclude_from_upload: .DS_.*?"
277
277
  setLocalFile(".DS_Store")
278
- Push.push
278
+ push
279
279
  noUploadsOccurred must beTrue
280
280
  }
281
281
  }
@@ -292,7 +292,7 @@ class S3WebsiteSpec extends Specification {
292
292
  | - logs
293
293
  """.stripMargin
294
294
  setLocalFiles(".DS_Store", "logs/test.log")
295
- Push.push
295
+ push
296
296
  noUploadsOccurred must beTrue
297
297
  }
298
298
  }
@@ -301,7 +301,7 @@ class S3WebsiteSpec extends Specification {
301
301
  "not delete the S3 objects that match the ignore value" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
302
302
  config = "ignore_on_server: logs"
303
303
  setS3Files(S3File("logs/log.txt", ""))
304
- Push.push
304
+ push
305
305
  noDeletesOccurred must beTrue
306
306
  }
307
307
  }
@@ -317,7 +317,7 @@ class S3WebsiteSpec extends Specification {
317
317
  | - .*txt
318
318
  """.stripMargin
319
319
  setS3Files(S3File("logs/log.txt", ""))
320
- Push.push
320
+ push
321
321
  noDeletesOccurred must beTrue
322
322
  }
323
323
  }
@@ -326,7 +326,7 @@ class S3WebsiteSpec extends Specification {
326
326
  "be applied to all files" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
327
327
  config = "max_age: 60"
328
328
  setLocalFile("index.html")
329
- Push.push
329
+ push
330
330
  sentPutObjectRequest.getMetadata.getCacheControl must equalTo("max-age=60")
331
331
  }
332
332
 
@@ -336,7 +336,7 @@ class S3WebsiteSpec extends Specification {
336
336
  | "*.html": 90
337
337
  """.stripMargin
338
338
  setLocalFile("index.html")
339
- Push.push
339
+ push
340
340
  sentPutObjectRequest.getMetadata.getCacheControl must equalTo("max-age=90")
341
341
  }
342
342
 
@@ -346,7 +346,7 @@ class S3WebsiteSpec extends Specification {
346
346
  | "assets/**/*.js": 90
347
347
  """.stripMargin
348
348
  setLocalFile("assets/lib/jquery.js")
349
- Push.push
349
+ push
350
350
  sentPutObjectRequest.getMetadata.getCacheControl must equalTo("max-age=90")
351
351
  }
352
352
 
@@ -356,14 +356,14 @@ class S3WebsiteSpec extends Specification {
356
356
  | "*.js": 90
357
357
  """.stripMargin
358
358
  setLocalFile("index.html")
359
- Push.push
359
+ push
360
360
  sentPutObjectRequest.getMetadata.getCacheControl must beNull
361
361
  }
362
362
 
363
363
  "be used to disable caching" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
364
364
  config = "max_age: 0"
365
365
  setLocalFile("index.html")
366
- Push.push
366
+ push
367
367
  sentPutObjectRequest.getMetadata.getCacheControl must equalTo("no-cache; max-age=0")
368
368
  }
369
369
  }
@@ -376,7 +376,7 @@ class S3WebsiteSpec extends Specification {
376
376
  | "assets/*.gif": 86400
377
377
  """.stripMargin
378
378
  setLocalFiles("assets/jquery.js", "assets/picture.gif")
379
- Push.push
379
+ push
380
380
  sentPutObjectRequests.find(_.getKey == "assets/jquery.js").get.getMetadata.getCacheControl must equalTo("max-age=150")
381
381
  sentPutObjectRequests.find(_.getKey == "assets/picture.gif").get.getMetadata.getCacheControl must equalTo("max-age=86400")
382
382
  }
@@ -386,7 +386,7 @@ class S3WebsiteSpec extends Specification {
386
386
  "result in uploads being marked with reduced redundancy" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
387
387
  config = "s3_reduced_redundancy: true"
388
388
  setLocalFile("file.exe")
389
- Push.push
389
+ push
390
390
  sentPutObjectRequest.getStorageClass must equalTo("REDUCED_REDUNDANCY")
391
391
  }
392
392
  }
@@ -395,7 +395,7 @@ class S3WebsiteSpec extends Specification {
395
395
  "result in uploads being marked with the default storage class" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
396
396
  config = "s3_reduced_redundancy: false"
397
397
  setLocalFile("file.exe")
398
- Push.push
398
+ push
399
399
  sentPutObjectRequest.getStorageClass must beNull
400
400
  }
401
401
  }
@@ -406,7 +406,7 @@ class S3WebsiteSpec extends Specification {
406
406
  |redirects:
407
407
  | index.php: /index.html
408
408
  """.stripMargin
409
- Push.push
409
+ push
410
410
  sentPutObjectRequest.getRedirectLocation must equalTo("/index.html")
411
411
  }
412
412
 
@@ -415,7 +415,7 @@ class S3WebsiteSpec extends Specification {
415
415
  |redirects:
416
416
  | index.php: /index.html
417
417
  """.stripMargin
418
- Push.push
418
+ push
419
419
  sentPutObjectRequest.getMetadata.getCacheControl must equalTo("max-age=0, no-cache")
420
420
  }
421
421
  }
@@ -428,7 +428,7 @@ class S3WebsiteSpec extends Specification {
428
428
  """.stripMargin
429
429
  setLocalFile("index.php")
430
430
  setS3Files(S3File("index.php", "md5"))
431
- Push.push
431
+ push
432
432
  noDeletesOccurred must beTrue
433
433
  }
434
434
  }
@@ -436,7 +436,7 @@ class S3WebsiteSpec extends Specification {
436
436
  "dotfiles" should {
437
437
  "be included in the pushed files" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
438
438
  setLocalFile(".vimrc")
439
- Push.push
439
+ push
440
440
  sentPutObjectRequest.getKey must equalTo(".vimrc")
441
441
  }
442
442
  }
@@ -444,25 +444,25 @@ class S3WebsiteSpec extends Specification {
444
444
  "content type inference" should {
445
445
  "add charset=utf-8 to all html documents" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
446
446
  setLocalFile("index.html")
447
- Push.push
447
+ push
448
448
  sentPutObjectRequest.getMetadata.getContentType must equalTo("text/html; charset=utf-8")
449
449
  }
450
450
 
451
451
  "add charset=utf-8 to all text documents" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
452
452
  setLocalFile("index.txt")
453
- Push.push
453
+ push
454
454
  sentPutObjectRequest.getMetadata.getContentType must equalTo("text/plain; charset=utf-8")
455
455
  }
456
456
 
457
457
  "add charset=utf-8 to all json documents" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
458
458
  setLocalFile("data.json")
459
- Push.push
459
+ push
460
460
  sentPutObjectRequest.getMetadata.getContentType must equalTo("application/json; charset=utf-8")
461
461
  }
462
462
 
463
463
  "resolve the content type from file contents" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
464
464
  setLocalFileWithContent(("index", "<html><body><h1>hi</h1></body></html>"))
465
- Push.push
465
+ push
466
466
  sentPutObjectRequest.getMetadata.getContentType must equalTo("text/html; charset=utf-8")
467
467
  }
468
468
  }
@@ -473,7 +473,7 @@ class S3WebsiteSpec extends Specification {
473
473
  |redirects:
474
474
  |<%= ('a'..'f').to_a.map do |t| ' '+t+ ': /'+t+'.html' end.join('\n')%>
475
475
  """.stripMargin
476
- Push.push
476
+ push
477
477
  sentPutObjectRequests.length must equalTo(6)
478
478
  sentPutObjectRequests.forall(_.getRedirectLocation != null) must beTrue
479
479
  }
@@ -483,7 +483,7 @@ class S3WebsiteSpec extends Specification {
483
483
  "not push updates" in new AllInSameDirectory with EmptySite with MockAWS with DryRunMode {
484
484
  setLocalFileWithContent(("index.html", "<div>new</div>"))
485
485
  setS3Files(S3File("index.html", md5Hex("<div>old</div>")))
486
- Push.push
486
+ push
487
487
  noUploadsOccurred must beTrue
488
488
  }
489
489
 
@@ -493,26 +493,26 @@ class S3WebsiteSpec extends Specification {
493
493
  |redirects:
494
494
  | index.php: /index.html
495
495
  """.stripMargin
496
- Push.push
496
+ push
497
497
  noUploadsOccurred must beTrue
498
498
  }
499
499
 
500
500
  "not push deletes" in new AllInSameDirectory with EmptySite with MockAWS with DryRunMode {
501
501
  setS3Files(S3File("index.html", md5Hex("<div>old</div>")))
502
- Push.push
502
+ push
503
503
  noUploadsOccurred must beTrue
504
504
  }
505
505
 
506
506
  "not push new files" in new AllInSameDirectory with EmptySite with MockAWS with DryRunMode {
507
507
  setLocalFile("index.html")
508
- Push.push
508
+ push
509
509
  noUploadsOccurred must beTrue
510
510
  }
511
511
 
512
512
  "not invalidate files" in new AllInSameDirectory with EmptySite with MockAWS with DryRunMode {
513
513
  config = "cloudfront_invalidation_id: AABBCC"
514
514
  setS3Files(S3File("index.html", md5Hex("<div>old</div>")))
515
- Push.push
515
+ push
516
516
  noInvalidationsOccurred must beTrue
517
517
  }
518
518
  }
@@ -520,7 +520,7 @@ class S3WebsiteSpec extends Specification {
520
520
  "Jekyll site" should {
521
521
  "be detected automatically" in new JekyllSite with EmptySite with MockAWS with DefaultRunMode {
522
522
  setLocalFile("index.html")
523
- Push.push
523
+ push
524
524
  sentPutObjectRequests.length must equalTo(1)
525
525
  }
526
526
  }
@@ -528,7 +528,7 @@ class S3WebsiteSpec extends Specification {
528
528
  "Nanoc site" should {
529
529
  "be detected automatically" in new NanocSite with EmptySite with MockAWS with DefaultRunMode {
530
530
  setLocalFile("index.html")
531
- Push.push
531
+ push
532
532
  sentPutObjectRequests.length must equalTo(1)
533
533
  }
534
534
  }
@@ -774,21 +774,4 @@ class S3WebsiteSpec extends Specification {
774
774
  buildSite(config)
775
775
  }
776
776
  }
777
-
778
- val defaultConfig = Config(
779
- s3_id = "foo",
780
- s3_secret = "bar",
781
- s3_bucket = "bucket",
782
- s3_endpoint = S3Endpoint.defaultEndpoint,
783
- max_age = None,
784
- gzip = None,
785
- gzip_zopfli = None,
786
- ignore_on_server = None,
787
- exclude_from_upload = None,
788
- s3_reduced_redundancy = None,
789
- cloudfront_distribution_id = None,
790
- cloudfront_invalidate_root = None,
791
- redirects = None,
792
- concurrency_level = 1
793
- )
794
777
  }
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.30
4
+ version: 0.0.31
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-13 00:00:00.000000000 Z
11
+ date: 2014-05-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -110,6 +110,7 @@ files:
110
110
  - resources/configuration_file_template.yml
111
111
  - s3_website.gemspec
112
112
  - sbt
113
+ - src/main/java/s3/website/ByteHelper.java
113
114
  - src/main/scala/s3/website/CloudFront.scala
114
115
  - src/main/scala/s3/website/Diff.scala
115
116
  - src/main/scala/s3/website/Push.scala