s3_website_monadic 0.0.30 → 0.0.31

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: 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