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