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 +4 -4
- data/build.sbt +4 -0
- data/changelog.md +10 -0
- data/lib/s3_website/version.rb +1 -1
- data/src/main/java/s3/website/ByteHelper.java +14 -0
- data/src/main/scala/s3/website/CloudFront.scala +1 -1
- data/src/main/scala/s3/website/Diff.scala +3 -1
- data/src/main/scala/s3/website/Push.scala +89 -65
- data/src/main/scala/s3/website/S3.scala +59 -18
- data/src/main/scala/s3/website/Utils.scala +1 -1
- data/src/main/scala/s3/website/model/push.scala +4 -2
- data/src/main/scala/s3/website/package.scala +1 -0
- data/src/test/scala/s3/website/S3WebsiteSpec.scala +58 -75
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 240e4d2a14000e5eed029857d69e426cf7439c4f
|
4
|
+
data.tar.gz: ad0d71c36c1f0a8add0cca08f1de9f49467c2019
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/s3_website/version.rb
CHANGED
@@ -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])
|
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) =>
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
case
|
149
|
-
|
150
|
-
|
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
|
-
|
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)
|
166
|
-
if (newFiles > 0)
|
167
|
-
if (failures > 0)
|
168
|
-
if (redirects > 0)
|
169
|
-
if (deletes > 0)
|
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
|
26
|
+
(implicit config: Config): Future[Either[FailedUpload, SuccessfulUpload]] =
|
23
27
|
Future {
|
24
28
|
val putObjectRequest = toPutObjectRequest(upload)
|
25
|
-
|
26
|
-
|
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
|
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} $
|
152
|
-
case Update => s"${Updated.renderVerb} $
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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-
|
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
|