s3_website 2.1.16 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +8 -12
- data/bin/s3_website +0 -2
- data/build.sbt +1 -3
- data/changelog.md +10 -0
- data/lib/s3_website/version.rb +1 -1
- data/resources/configuration_file_template.yml +2 -0
- data/src/main/scala/s3/website/ByteHelper.scala +18 -0
- data/src/main/scala/s3/website/Push.scala +5 -9
- data/src/main/scala/s3/website/model/Config.scala +9 -2
- data/src/main/scala/s3/website/model/Site.scala +45 -12
- data/src/main/scala/s3/website/model/push.scala +1 -1
- data/src/main/scala/s3/website/model/ssg.scala +4 -10
- data/src/test/scala/s3/website/S3WebsiteSpec.scala +164 -116
- metadata +3 -3
- data/src/main/java/s3/website/ByteHelper.java +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a0db818cde92ba72ae2bca269f676c18e63277ac
|
4
|
+
data.tar.gz: 20315d8fde0ef16cad751cf104f45eea931612d0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0229a6d300c77da0ea0e01d151994c26fd7cd62fd84e64fdf6c58e7b1ee087b5a6f29bd8667ec7432efd3a6dfbf058facfb07aa90e764840be78c32aeb2c4fef
|
7
|
+
data.tar.gz: 03fd97df2738bd7a8e0b09f37e759d7de45b3075932bf828cdda04eae2667eb7f547b8563a4bb2b80e2d4c8a1acbbd36c892bc0fd4b9f8522dbd013febcc0abe
|
data/README.md
CHANGED
@@ -33,23 +33,19 @@ Here's how you can get started:
|
|
33
33
|
S3 website. If the bucket does not exist, the command will create it for you.
|
34
34
|
* Run `s3_website push` to push your website to S3. Congratulations! You are live.
|
35
35
|
|
36
|
-
###
|
36
|
+
### Specifying the location of your website
|
37
37
|
|
38
|
-
S3_website will automatically discover
|
38
|
+
S3_website will automatically discover websites in the *_site* and
|
39
|
+
*public/output* directories.
|
39
40
|
|
40
|
-
|
41
|
+
If your website is not in either of those directories, you can
|
42
|
+
point the location of your website in two ways:
|
41
43
|
|
42
|
-
|
43
|
-
|
44
|
-
### For others
|
45
|
-
|
46
|
-
It's a good idea to store the `s3_website.yml` file in your project's root.
|
47
|
-
Let's say the contents you wish to upload to your S3 website bucket are in
|
48
|
-
*my_website_output*. You can upload the contents of that directory with
|
49
|
-
`s3_website push --site my_website_output`.
|
44
|
+
1. Add the line `site: path-to-your-website` into the `s3_website.yml` file
|
45
|
+
2. Or, use the `--site=path-to-your-site` command-line argument
|
50
46
|
|
51
47
|
If you want to store the `s3_website.yml` file in a directory other than
|
52
|
-
the project's root you can specify the directory
|
48
|
+
the project's root you can specify the directory like so:
|
53
49
|
`s3_website push --config-dir config`.
|
54
50
|
|
55
51
|
### Using environment variables
|
data/bin/s3_website
CHANGED
data/build.sbt
CHANGED
@@ -4,7 +4,7 @@ name := "s3_website"
|
|
4
4
|
|
5
5
|
version := "0.0.1"
|
6
6
|
|
7
|
-
scalaVersion := "2.11.
|
7
|
+
scalaVersion := "2.11.2"
|
8
8
|
|
9
9
|
scalacOptions += "-feature"
|
10
10
|
|
@@ -14,8 +14,6 @@ scalacOptions += "-language:postfixOps"
|
|
14
14
|
|
15
15
|
scalacOptions += "-target:jvm-1.6"
|
16
16
|
|
17
|
-
javacOptions in Compile ++= Seq("-source", "1.6", "-target", "1.6")
|
18
|
-
|
19
17
|
libraryDependencies += "org.yaml" % "snakeyaml" % "1.13"
|
20
18
|
|
21
19
|
libraryDependencies += "org.jruby" % "jruby" % "1.7.11"
|
data/changelog.md
CHANGED
@@ -2,6 +2,16 @@
|
|
2
2
|
|
3
3
|
This project uses [Semantic Versioning](http://semver.org).
|
4
4
|
|
5
|
+
## 2.2.0
|
6
|
+
|
7
|
+
* Specify the location of the website in the *s3_website.yml* file
|
8
|
+
|
9
|
+
Just add the setting `site: path-to-your-website` into the file.
|
10
|
+
|
11
|
+
* Fix Nanoc auto detection
|
12
|
+
|
13
|
+
Previously, a website in *public/output* was not automatically detected.
|
14
|
+
|
5
15
|
## 2.1.16
|
6
16
|
|
7
17
|
* Support non-US-ASCII files when using `ignore_on_server`
|
data/lib/s3_website/version.rb
CHANGED
@@ -0,0 +1,18 @@
|
|
1
|
+
package s3.website
|
2
|
+
|
3
|
+
object ByteHelper {
|
4
|
+
|
5
|
+
// Adapted from http://stackoverflow.com/a/3758880/219947
|
6
|
+
def humanReadableByteCount(bytes: Long): String = {
|
7
|
+
val si: Boolean = true
|
8
|
+
val unit: Int = if (si) 1000 else 1024
|
9
|
+
if (bytes < unit) {
|
10
|
+
bytes + " B"
|
11
|
+
} else {
|
12
|
+
val exp: Int = (Math.log(bytes) / Math.log(unit)).asInstanceOf[Int]
|
13
|
+
val pre: String = (if (si) "kMGTPE" else "KMGTPE").charAt(exp - 1) + (if (si) "" else "i")
|
14
|
+
val formatArgs = (bytes / Math.pow(unit, exp)).asInstanceOf[AnyRef] :: pre :: Nil
|
15
|
+
String.format("%.1f %sB", formatArgs.toArray:_*)
|
16
|
+
}
|
17
|
+
}
|
18
|
+
}
|
@@ -1,5 +1,6 @@
|
|
1
1
|
package s3.website
|
2
2
|
|
3
|
+
import s3.website.model.Config.S3_website_yml
|
3
4
|
import s3.website.model.Site._
|
4
5
|
import scala.concurrent.{ExecutionContextExecutor, Future, Await}
|
5
6
|
import scala.concurrent.duration._
|
@@ -21,7 +22,6 @@ import s3.website.S3.S3Setting
|
|
21
22
|
import s3.website.CloudFront.CloudFrontSetting
|
22
23
|
import s3.website.S3.SuccessfulUpload
|
23
24
|
import s3.website.CloudFront.FailedInvalidation
|
24
|
-
import scala.Int
|
25
25
|
import java.io.File
|
26
26
|
import com.lexicalscope.jewel.cli.CliFactory.parseArguments
|
27
27
|
import s3.website.ByteHelper.humanReadableByteCount
|
@@ -52,15 +52,11 @@ object Push {
|
|
52
52
|
def dryRun = cliArgs.dryRun
|
53
53
|
}
|
54
54
|
|
55
|
-
val
|
56
|
-
Option(cliArgs.site).fold(Ssg.findSiteDirectory(workingDirectory))(siteDirFromCli => Right(new File(siteDirFromCli)))
|
57
|
-
def errorOrSite(siteInDirectory: File): Either[ErrorReport, Site] =
|
58
|
-
loadSite(Option(cliArgs.configDir).getOrElse(workingDirectory.getPath) + "/s3_website.yml", siteInDirectory.getAbsolutePath)
|
55
|
+
implicit val yamlConfig = S3_website_yml(new File(Option(cliArgs.configDir).getOrElse(workingDirectory.getPath) + "/s3_website.yml"))
|
59
56
|
|
60
|
-
val errorOrPushStatus = for
|
61
|
-
|
62
|
-
|
63
|
-
} yield {
|
57
|
+
val errorOrPushStatus = for (
|
58
|
+
loadedSite <- loadSite.right
|
59
|
+
) yield {
|
64
60
|
implicit val site = loadedSite
|
65
61
|
val threadPool = newFixedThreadPool(site.config.concurrency_level)
|
66
62
|
implicit val executor = fromExecutor(threadPool)
|
@@ -1,5 +1,7 @@
|
|
1
1
|
package s3.website.model
|
2
2
|
|
3
|
+
import java.io.File
|
4
|
+
|
3
5
|
import scala.util.{Failure, Try}
|
4
6
|
import scala.collection.JavaConversions._
|
5
7
|
import s3.website.Ruby.rubyRuntime
|
@@ -11,6 +13,7 @@ case class Config(
|
|
11
13
|
s3_secret: Option[String], // If undefined, use IAM Roles (http://docs.aws.amazon.com/AWSSdkDocsJava/latest/DeveloperGuide/java-dg-roles.html)
|
12
14
|
s3_bucket: String,
|
13
15
|
s3_endpoint: S3Endpoint,
|
16
|
+
site: Option[String],
|
14
17
|
max_age: Option[Either[Int, Map[String, Int]]],
|
15
18
|
gzip: Option[Either[Boolean, Seq[String]]],
|
16
19
|
gzip_zopfli: Option[Boolean],
|
@@ -154,7 +157,7 @@ object Config {
|
|
154
157
|
unsafeYaml.yamlObject.asInstanceOf[java.util.Map[String, _]].toMap get key
|
155
158
|
}
|
156
159
|
|
157
|
-
def erbEval(erbString: String,
|
160
|
+
def erbEval(erbString: String, yamlConfig: S3_website_yml): Try[String] = Try {
|
158
161
|
val erbStringWithoutComments = erbString.replaceAll("^\\s*#.*", "")
|
159
162
|
rubyRuntime.evalScriptlet(
|
160
163
|
s"""|# encoding: utf-8
|
@@ -167,9 +170,13 @@ object Config {
|
|
167
170
|
""".stripMargin
|
168
171
|
).asJavaString()
|
169
172
|
} match {
|
170
|
-
case Failure(err) => Failure(new RuntimeException(s"Failed to parse ERB in $
|
173
|
+
case Failure(err) => Failure(new RuntimeException(s"Failed to parse ERB in $yamlConfig:\n${err.getMessage}"))
|
171
174
|
case x => x
|
172
175
|
}
|
173
176
|
|
174
177
|
case class UnsafeYaml(yamlObject: AnyRef)
|
178
|
+
|
179
|
+
case class S3_website_yml(file: File) {
|
180
|
+
override def toString = file.getPath
|
181
|
+
}
|
175
182
|
}
|
@@ -1,6 +1,9 @@
|
|
1
1
|
package s3.website.model
|
2
2
|
|
3
3
|
import java.io.File
|
4
|
+
import s3.website.Push.CliArgs
|
5
|
+
import s3.website.model.Ssg.autodetectSiteDir
|
6
|
+
|
4
7
|
import scala.util.Try
|
5
8
|
import org.yaml.snakeyaml.Yaml
|
6
9
|
import s3.website.model.Config._
|
@@ -11,8 +14,8 @@ import scala.util.Failure
|
|
11
14
|
import s3.website.model.Config.UnsafeYaml
|
12
15
|
import scala.util.Success
|
13
16
|
|
14
|
-
case class Site(rootDirectory:
|
15
|
-
def resolveS3Key(file: File) = file.getAbsolutePath.replace(rootDirectory, "").replaceFirst("^/", "")
|
17
|
+
case class Site(rootDirectory: File, config: Config) {
|
18
|
+
def resolveS3Key(file: File) = file.getAbsolutePath.replace(rootDirectory.getAbsolutePath, "").replaceFirst("^/", "")
|
16
19
|
|
17
20
|
def resolveFile(s3File: S3File): File = resolveFile(s3File.s3Key)
|
18
21
|
|
@@ -20,21 +23,22 @@ case class Site(rootDirectory: String, config: Config) {
|
|
20
23
|
}
|
21
24
|
|
22
25
|
object Site {
|
23
|
-
def
|
24
|
-
(implicit logger: Logger): Either[ErrorReport, Site] = {
|
26
|
+
def parseConfig(implicit logger: Logger, yamlConfig: S3_website_yml): Either[ErrorReport, Config] = {
|
25
27
|
val yamlObjectTry = for {
|
26
|
-
yamlString <- Try(fromFile(
|
27
|
-
yamlWithErbEvaluated <- erbEval(yamlString,
|
28
|
+
yamlString <- Try(fromFile(yamlConfig.file).mkString)
|
29
|
+
yamlWithErbEvaluated <- erbEval(yamlString, yamlConfig)
|
28
30
|
yamlObject <- Try(new Yaml() load yamlWithErbEvaluated)
|
29
31
|
} yield yamlObject
|
32
|
+
|
30
33
|
yamlObjectTry match {
|
31
34
|
case Success(yamlObject) =>
|
32
35
|
implicit val unsafeYaml = UnsafeYaml(yamlObject)
|
33
|
-
|
36
|
+
for {
|
34
37
|
s3_id <- loadOptionalString("s3_id").right
|
35
38
|
s3_secret <- loadOptionalString("s3_secret").right
|
36
39
|
s3_bucket <- loadRequiredString("s3_bucket").right
|
37
40
|
s3_endpoint <- loadEndpoint.right
|
41
|
+
site <- loadOptionalString("site").right
|
38
42
|
max_age <- loadMaxAge.right
|
39
43
|
gzip <- loadOptionalBooleanOrStringSeq("gzip").right
|
40
44
|
gzip_zopfli <- loadOptionalBoolean("gzip_zopfli").right
|
@@ -49,16 +53,17 @@ object Site {
|
|
49
53
|
} yield {
|
50
54
|
gzip_zopfli.foreach(_ => logger.info(
|
51
55
|
"""|Zopfli is not currently supported. Falling back to regular gzip.
|
52
|
-
|
53
|
-
|
56
|
+
|If you find a stable Java implementation for zopfli, please send an email to lauri.lehmijoki@iki.fi about it."""
|
57
|
+
.stripMargin))
|
54
58
|
extensionless_mime_type.foreach(_ => logger.info(
|
55
|
-
s"Ignoring the extensionless_mime_type setting in $
|
59
|
+
s"Ignoring the extensionless_mime_type setting in $yamlConfig. Counting on Apache Tika to infer correct mime types.")
|
56
60
|
)
|
57
61
|
Config(
|
58
62
|
s3_id,
|
59
63
|
s3_secret,
|
60
64
|
s3_bucket,
|
61
65
|
s3_endpoint getOrElse S3Endpoint.defaultEndpoint,
|
66
|
+
site,
|
62
67
|
max_age,
|
63
68
|
gzip,
|
64
69
|
gzip_zopfli,
|
@@ -71,10 +76,38 @@ object Site {
|
|
71
76
|
concurrency_level.fold(20)(_ max 20) // At minimum, run 20 concurrent operations
|
72
77
|
)
|
73
78
|
}
|
74
|
-
|
75
|
-
config.right.map(Site(siteRootDirectory, _))
|
76
79
|
case Failure(error) =>
|
77
80
|
Left(ErrorReport(error))
|
78
81
|
}
|
79
82
|
}
|
83
|
+
|
84
|
+
def loadSite(implicit yamlConfig: S3_website_yml, cliArgs: CliArgs, workingDirectory: File, logger: Logger): Either[ErrorReport, Site] =
|
85
|
+
parseConfig.right.flatMap { cfg =>
|
86
|
+
implicit val config: Config = cfg
|
87
|
+
val errorOrSiteDir = resolveSiteDir.fold(Left(ErrorReport(noSiteFound)): Either[ErrorReport, File])(Right(_))
|
88
|
+
errorOrSiteDir.right.map(Site(_, config))
|
89
|
+
}
|
90
|
+
|
91
|
+
val noSiteFound =
|
92
|
+
"""|Could not find a website.
|
93
|
+
|Either use the --site=DIR command-line argument or define the location of the site in s3_website.yml.
|
94
|
+
|
|
95
|
+
|Here's an example of how you can define the site directory in s3_website.yml:
|
96
|
+
| site: dist/website""".stripMargin
|
97
|
+
|
98
|
+
def resolveSiteDir(implicit yamlConfig: S3_website_yml, config: Config, cliArgs: CliArgs, workingDirectory: File): Option[File] = {
|
99
|
+
val siteFromAutoDetect = autodetectSiteDir(workingDirectory)
|
100
|
+
val siteFromCliArgs = Option(cliArgs.site).map(new File(_))
|
101
|
+
|
102
|
+
siteFromCliArgs orElse siteFromConfig orElse siteFromAutoDetect
|
103
|
+
}
|
104
|
+
|
105
|
+
def siteFromConfig(implicit yamlConfig: S3_website_yml, config: Config, workingDirectory: File): Option[File] =
|
106
|
+
config
|
107
|
+
.site
|
108
|
+
.map(new File(_))
|
109
|
+
.map { siteDir =>
|
110
|
+
if (siteDir.isAbsolute) siteDir
|
111
|
+
else new File(yamlConfig.file.getParentFile, siteDir.getPath)
|
112
|
+
}
|
80
113
|
}
|
@@ -142,7 +142,7 @@ object Files {
|
|
142
142
|
if (doNotUpload) logger.debug(s"Excluded $s3Key from upload")
|
143
143
|
doNotUpload
|
144
144
|
}
|
145
|
-
recursiveListFiles(
|
145
|
+
recursiveListFiles(site.rootDirectory)
|
146
146
|
.filterNot(_.isDirectory)
|
147
147
|
.filterNot(f => excludeFromUpload(site.resolveS3Key(f)))
|
148
148
|
}
|
@@ -1,6 +1,7 @@
|
|
1
1
|
package s3.website.model
|
2
2
|
|
3
3
|
import java.io.File
|
4
|
+
import s3.website.model.Files.recursiveListFiles
|
4
5
|
import s3.website.{ErrorOrFile, ErrorReport}
|
5
6
|
|
6
7
|
// ssg = static site generator
|
@@ -11,17 +12,10 @@ trait Ssg {
|
|
11
12
|
object Ssg {
|
12
13
|
val automaticallySupportedSiteGenerators = Jekyll :: Nanoc :: Nil
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
"""|Could not find a website in any of the pre-defined directories.
|
18
|
-
|Specify the website location with the --site=path argument and try again.""".stripMargin
|
15
|
+
def autodetectSiteDir(workingDirectory: File): Option[File] =
|
16
|
+
recursiveListFiles(workingDirectory).find { file =>
|
17
|
+
file.isDirectory && automaticallySupportedSiteGenerators.exists(ssg => file.getAbsolutePath.endsWith(ssg.outputDirectory))
|
19
18
|
}
|
20
|
-
|
21
|
-
def findSiteDirectory(workingDirectory: File): ErrorOrFile =
|
22
|
-
Files.recursiveListFiles(workingDirectory).find { file =>
|
23
|
-
file.isDirectory && automaticallySupportedSiteGenerators.exists(_.outputDirectory == file.getName)
|
24
|
-
}.fold(Left(notFoundErrorReport): ErrorOrFile)(Right(_))
|
25
19
|
}
|
26
20
|
|
27
21
|
case object Jekyll extends Ssg {
|
@@ -1,40 +1,37 @@
|
|
1
1
|
package s3.website
|
2
2
|
|
3
|
-
import org.specs2.mutable.{BeforeAfter, After, Specification}
|
4
|
-
import s3.website.model._
|
5
|
-
import org.specs2.specification.Scope
|
6
|
-
import org.apache.commons.io.FileUtils
|
7
3
|
import java.io.File
|
8
|
-
import
|
9
|
-
|
10
|
-
import
|
4
|
+
import java.util.concurrent.atomic.AtomicInteger
|
5
|
+
|
6
|
+
import com.amazonaws.AmazonServiceException
|
7
|
+
import com.amazonaws.services.cloudfront.AmazonCloudFront
|
8
|
+
import com.amazonaws.services.cloudfront.model.{CreateInvalidationRequest, CreateInvalidationResult, TooManyInvalidationsInProgressException}
|
11
9
|
import com.amazonaws.services.s3.AmazonS3
|
12
10
|
import com.amazonaws.services.s3.model._
|
13
|
-
import scala.concurrent.duration._
|
14
|
-
import s3.website.S3.S3Setting
|
15
|
-
import scala.collection.JavaConversions._
|
16
|
-
import com.amazonaws.AmazonServiceException
|
17
11
|
import org.apache.commons.codec.digest.DigestUtils._
|
18
|
-
import
|
19
|
-
import com.amazonaws.services.cloudfront.AmazonCloudFront
|
20
|
-
import com.amazonaws.services.cloudfront.model.{CreateInvalidationResult, CreateInvalidationRequest, TooManyInvalidationsInProgressException}
|
21
|
-
import org.mockito.stubbing.Answer
|
22
|
-
import org.mockito.invocation.InvocationOnMock
|
23
|
-
import java.util.concurrent.atomic.AtomicInteger
|
12
|
+
import org.apache.commons.io.FileUtils
|
24
13
|
import org.apache.commons.io.FileUtils._
|
25
|
-
import
|
26
|
-
import
|
27
|
-
import
|
28
|
-
import
|
29
|
-
import org.
|
30
|
-
import
|
14
|
+
import org.mockito.Mockito._
|
15
|
+
import org.mockito.invocation.InvocationOnMock
|
16
|
+
import org.mockito.stubbing.Answer
|
17
|
+
import org.mockito.{ArgumentCaptor, Matchers, Mockito}
|
18
|
+
import org.specs2.mutable.{BeforeAfter, Specification}
|
19
|
+
import org.specs2.specification.Scope
|
31
20
|
import s3.website.CloudFront.CloudFrontSetting
|
21
|
+
import s3.website.Push.{CliArgs}
|
32
22
|
import s3.website.S3.S3Setting
|
23
|
+
import s3.website.model.Config.S3_website_yml
|
24
|
+
import s3.website.model.Ssg.automaticallySupportedSiteGenerators
|
25
|
+
import s3.website.model._
|
26
|
+
|
27
|
+
import scala.collection.JavaConversions._
|
28
|
+
import scala.concurrent.duration._
|
29
|
+
import scala.util.Random
|
33
30
|
|
34
31
|
class S3WebsiteSpec extends Specification {
|
35
32
|
|
36
33
|
"gzip: true" should {
|
37
|
-
"update a gzipped S3 object if the contents has changed" in new
|
34
|
+
"update a gzipped S3 object if the contents has changed" in new BasicSetup {
|
38
35
|
config = "gzip: true"
|
39
36
|
setLocalFileWithContent(("styles.css", "<h1>hi again</h1>"))
|
40
37
|
setS3Files(S3File("styles.css", "1c5117e5839ad8fc00ce3c41296255a1" /* md5 of the gzip of the file contents */))
|
@@ -42,7 +39,7 @@ class S3WebsiteSpec extends Specification {
|
|
42
39
|
sentPutObjectRequest.getKey must equalTo("styles.css")
|
43
40
|
}
|
44
41
|
|
45
|
-
"not update a gzipped S3 object if the contents has not changed" in new
|
42
|
+
"not update a gzipped S3 object if the contents has not changed" in new BasicSetup {
|
46
43
|
config = "gzip: true"
|
47
44
|
setLocalFileWithContent(("styles.css", "<h1>hi</h1>"))
|
48
45
|
setS3Files(S3File("styles.css", "1c5117e5839ad8fc00ce3c41296255a1" /* md5 of the gzip of the file contents */))
|
@@ -55,7 +52,7 @@ class S3WebsiteSpec extends Specification {
|
|
55
52
|
gzip:
|
56
53
|
- .xml
|
57
54
|
""" should {
|
58
|
-
"update a gzipped S3 object if the contents has changed" in new
|
55
|
+
"update a gzipped S3 object if the contents has changed" in new BasicSetup {
|
59
56
|
config = """
|
60
57
|
|gzip:
|
61
58
|
| - .xml
|
@@ -68,40 +65,40 @@ class S3WebsiteSpec extends Specification {
|
|
68
65
|
}
|
69
66
|
|
70
67
|
"push" should {
|
71
|
-
"not upload a file if it has not changed" in new
|
68
|
+
"not upload a file if it has not changed" in new BasicSetup {
|
72
69
|
setLocalFileWithContent(("index.html", "<div>hello</div>"))
|
73
70
|
setS3Files(S3File("index.html", md5Hex("<div>hello</div>")))
|
74
71
|
push
|
75
72
|
noUploadsOccurred must beTrue
|
76
73
|
}
|
77
74
|
|
78
|
-
"update a file if it has changed" in new
|
75
|
+
"update a file if it has changed" in new BasicSetup {
|
79
76
|
setLocalFileWithContent(("index.html", "<h1>old text</h1>"))
|
80
77
|
setS3Files(S3File("index.html", md5Hex("<h1>new text</h1>")))
|
81
78
|
push
|
82
79
|
sentPutObjectRequest.getKey must equalTo("index.html")
|
83
80
|
}
|
84
81
|
|
85
|
-
"create a file if does not exist on S3" in new
|
82
|
+
"create a file if does not exist on S3" in new BasicSetup {
|
86
83
|
setLocalFile("index.html")
|
87
84
|
push
|
88
85
|
sentPutObjectRequest.getKey must equalTo("index.html")
|
89
86
|
}
|
90
87
|
|
91
|
-
"delete files that are on S3 but not on local file system" in new
|
88
|
+
"delete files that are on S3 but not on local file system" in new BasicSetup {
|
92
89
|
setS3Files(S3File("old.html", md5Hex("<h1>old text</h1>")))
|
93
90
|
push
|
94
91
|
sentDelete must equalTo("old.html")
|
95
92
|
}
|
96
93
|
|
97
|
-
"try again if the upload fails" in new
|
94
|
+
"try again if the upload fails" in new BasicSetup {
|
98
95
|
setLocalFile("index.html")
|
99
96
|
uploadFailsAndThenSucceeds(howManyFailures = 5)
|
100
97
|
push
|
101
98
|
verify(amazonS3Client, times(6)).putObject(Matchers.any(classOf[PutObjectRequest]))
|
102
99
|
}
|
103
100
|
|
104
|
-
"not try again if the upload fails on because of invalid credentials" in new
|
101
|
+
"not try again if the upload fails on because of invalid credentials" in new BasicSetup {
|
105
102
|
setLocalFile("index.html")
|
106
103
|
when(amazonS3Client.putObject(Matchers.any(classOf[PutObjectRequest]))).thenThrow {
|
107
104
|
val e = new AmazonServiceException("your credentials are incorrect")
|
@@ -112,7 +109,7 @@ class S3WebsiteSpec extends Specification {
|
|
112
109
|
verify(amazonS3Client, times(1)).putObject(Matchers.any(classOf[PutObjectRequest]))
|
113
110
|
}
|
114
111
|
|
115
|
-
"try again if the request times out" in new
|
112
|
+
"try again if the request times out" in new BasicSetup {
|
116
113
|
var attempt = 0
|
117
114
|
when(amazonS3Client putObject Matchers.any(classOf[PutObjectRequest])) thenAnswer new Answer[PutObjectResult] {
|
118
115
|
def answer(invocation: InvocationOnMock) = {
|
@@ -132,14 +129,14 @@ class S3WebsiteSpec extends Specification {
|
|
132
129
|
verify(amazonS3Client, times(2)).putObject(Matchers.any(classOf[PutObjectRequest]))
|
133
130
|
}
|
134
131
|
|
135
|
-
"try again if the delete fails" in new
|
132
|
+
"try again if the delete fails" in new BasicSetup {
|
136
133
|
setS3Files(S3File("old.html", md5Hex("<h1>old text</h1>")))
|
137
134
|
deleteFailsAndThenSucceeds(howManyFailures = 5)
|
138
135
|
push
|
139
136
|
verify(amazonS3Client, times(6)).deleteObject(Matchers.anyString(), Matchers.anyString())
|
140
137
|
}
|
141
138
|
|
142
|
-
"try again if the object listing fails" in new
|
139
|
+
"try again if the object listing fails" in new BasicSetup {
|
143
140
|
setS3Files(S3File("old.html", md5Hex("<h1>old text</h1>")))
|
144
141
|
objectListingFailsAndThenSucceeds(howManyFailures = 5)
|
145
142
|
push
|
@@ -148,7 +145,7 @@ class S3WebsiteSpec extends Specification {
|
|
148
145
|
}
|
149
146
|
|
150
147
|
"push with CloudFront" should {
|
151
|
-
"invalidate the updated CloudFront items" in new
|
148
|
+
"invalidate the updated CloudFront items" in new BasicSetup {
|
152
149
|
config = "cloudfront_distribution_id: EGM1J2JJX9Z"
|
153
150
|
setLocalFiles("css/test.css", "articles/index.html")
|
154
151
|
setOutdatedS3Keys("css/test.css", "articles/index.html")
|
@@ -156,14 +153,14 @@ class S3WebsiteSpec extends Specification {
|
|
156
153
|
sentInvalidationRequest.getInvalidationBatch.getPaths.getItems.toSeq.sorted must equalTo(("/css/test.css" :: "/articles/index.html" :: Nil).sorted)
|
157
154
|
}
|
158
155
|
|
159
|
-
"not send CloudFront invalidation requests on new objects" in new
|
156
|
+
"not send CloudFront invalidation requests on new objects" in new BasicSetup {
|
160
157
|
config = "cloudfront_distribution_id: EGM1J2JJX9Z"
|
161
158
|
setLocalFile("newfile.js")
|
162
159
|
push
|
163
160
|
noInvalidationsOccurred must beTrue
|
164
161
|
}
|
165
162
|
|
166
|
-
"not send CloudFront invalidation requests on redirect objects" in new
|
163
|
+
"not send CloudFront invalidation requests on redirect objects" in new BasicSetup {
|
167
164
|
config = """
|
168
165
|
|cloudfront_distribution_id: EGM1J2JJX9Z
|
169
166
|
|redirects:
|
@@ -173,7 +170,7 @@ class S3WebsiteSpec extends Specification {
|
|
173
170
|
noInvalidationsOccurred must beTrue
|
174
171
|
}
|
175
172
|
|
176
|
-
"retry CloudFront responds with TooManyInvalidationsInProgressException" in new
|
173
|
+
"retry CloudFront responds with TooManyInvalidationsInProgressException" in new BasicSetup {
|
177
174
|
setTooManyInvalidationsInProgress(4)
|
178
175
|
config = "cloudfront_distribution_id: EGM1J2JJX9Z"
|
179
176
|
setLocalFile("test.css")
|
@@ -182,7 +179,7 @@ class S3WebsiteSpec extends Specification {
|
|
182
179
|
sentInvalidationRequests.length must equalTo(4)
|
183
180
|
}
|
184
181
|
|
185
|
-
"retry if CloudFront is temporarily unreachable" in new
|
182
|
+
"retry if CloudFront is temporarily unreachable" in new BasicSetup {
|
186
183
|
invalidationsFailAndThenSucceed(5)
|
187
184
|
config = "cloudfront_distribution_id: EGM1J2JJX9Z"
|
188
185
|
setLocalFile("test.css")
|
@@ -191,7 +188,7 @@ class S3WebsiteSpec extends Specification {
|
|
191
188
|
sentInvalidationRequests.length must equalTo(6)
|
192
189
|
}
|
193
190
|
|
194
|
-
"encode unsafe characters in the keys" in new
|
191
|
+
"encode unsafe characters in the keys" in new BasicSetup {
|
195
192
|
config = "cloudfront_distribution_id: EGM1J2JJX9Z"
|
196
193
|
setLocalFile("articles/arnold's file.html")
|
197
194
|
setOutdatedS3Keys("articles/arnold's file.html")
|
@@ -199,7 +196,7 @@ class S3WebsiteSpec extends Specification {
|
|
199
196
|
sentInvalidationRequest.getInvalidationBatch.getPaths.getItems.toSeq.sorted must equalTo(("/articles/arnold's%20file.html" :: Nil).sorted)
|
200
197
|
}
|
201
198
|
|
202
|
-
"invalidate the root object '/' if a top-level object is updated or deleted" in new
|
199
|
+
"invalidate the root object '/' if a top-level object is updated or deleted" in new BasicSetup {
|
203
200
|
config = "cloudfront_distribution_id: EGM1J2JJX9Z"
|
204
201
|
setLocalFile("maybe-index.html")
|
205
202
|
setOutdatedS3Keys("maybe-index.html")
|
@@ -209,7 +206,7 @@ class S3WebsiteSpec extends Specification {
|
|
209
206
|
}
|
210
207
|
|
211
208
|
"cloudfront_invalidate_root: true" should {
|
212
|
-
"convert CloudFront invalidation paths with the '/index.html' suffix into '/'" in new
|
209
|
+
"convert CloudFront invalidation paths with the '/index.html' suffix into '/'" in new BasicSetup {
|
213
210
|
config = """
|
214
211
|
|cloudfront_distribution_id: EGM1J2JJX9Z
|
215
212
|
|cloudfront_invalidate_root: true
|
@@ -222,7 +219,7 @@ class S3WebsiteSpec extends Specification {
|
|
222
219
|
}
|
223
220
|
|
224
221
|
"a site with over 1000 items" should {
|
225
|
-
"split the CloudFront invalidation requests into batches of 1000 items" in new
|
222
|
+
"split the CloudFront invalidation requests into batches of 1000 items" in new BasicSetup {
|
226
223
|
val files = (1 to 1002).map { i => s"lots-of-files/file-$i"}
|
227
224
|
config = "cloudfront_distribution_id: EGM1J2JJX9Z"
|
228
225
|
setLocalFiles(files:_*)
|
@@ -235,18 +232,18 @@ class S3WebsiteSpec extends Specification {
|
|
235
232
|
}
|
236
233
|
|
237
234
|
"push exit status" should {
|
238
|
-
"be 0 all uploads succeed" in new
|
235
|
+
"be 0 all uploads succeed" in new BasicSetup {
|
239
236
|
setLocalFiles("file.txt")
|
240
237
|
push must equalTo(0)
|
241
238
|
}
|
242
239
|
|
243
|
-
"be 1 if any of the uploads fails" in new
|
240
|
+
"be 1 if any of the uploads fails" in new BasicSetup {
|
244
241
|
setLocalFiles("file.txt")
|
245
242
|
when(amazonS3Client.putObject(Matchers.any(classOf[PutObjectRequest]))).thenThrow(new AmazonServiceException("AWS failed"))
|
246
243
|
push must equalTo(1)
|
247
244
|
}
|
248
245
|
|
249
|
-
"be 1 if any of the redirects fails" in new
|
246
|
+
"be 1 if any of the redirects fails" in new BasicSetup {
|
250
247
|
config = """
|
251
248
|
|redirects:
|
252
249
|
| index.php: /index.html
|
@@ -255,14 +252,14 @@ class S3WebsiteSpec extends Specification {
|
|
255
252
|
push must equalTo(1)
|
256
253
|
}
|
257
254
|
|
258
|
-
"be 0 if CloudFront invalidations and uploads succeed"in new
|
255
|
+
"be 0 if CloudFront invalidations and uploads succeed"in new BasicSetup {
|
259
256
|
config = "cloudfront_distribution_id: EGM1J2JJX9Z"
|
260
257
|
setLocalFile("test.css")
|
261
258
|
setOutdatedS3Keys("test.css")
|
262
259
|
push must equalTo(0)
|
263
260
|
}
|
264
261
|
|
265
|
-
"be 1 if CloudFront is unreachable or broken"in new
|
262
|
+
"be 1 if CloudFront is unreachable or broken"in new BasicSetup {
|
266
263
|
setCloudFrontAsInternallyBroken()
|
267
264
|
config = "cloudfront_distribution_id: EGM1J2JJX9Z"
|
268
265
|
setLocalFile("test.css")
|
@@ -270,19 +267,19 @@ class S3WebsiteSpec extends Specification {
|
|
270
267
|
push must equalTo(1)
|
271
268
|
}
|
272
269
|
|
273
|
-
"be 0 if upload retry succeeds" in new
|
270
|
+
"be 0 if upload retry succeeds" in new BasicSetup {
|
274
271
|
setLocalFile("index.html")
|
275
272
|
uploadFailsAndThenSucceeds(howManyFailures = 1)
|
276
273
|
push must equalTo(0)
|
277
274
|
}
|
278
275
|
|
279
|
-
"be 1 if delete retry fails" in new
|
276
|
+
"be 1 if delete retry fails" in new BasicSetup {
|
280
277
|
setLocalFile("index.html")
|
281
278
|
uploadFailsAndThenSucceeds(howManyFailures = 6)
|
282
279
|
push must equalTo(1)
|
283
280
|
}
|
284
281
|
|
285
|
-
"be 1 if an object listing fails" in new
|
282
|
+
"be 1 if an object listing fails" in new BasicSetup {
|
286
283
|
setS3Files(S3File("old.html", md5Hex("<h1>old text</h1>")))
|
287
284
|
objectListingFailsAndThenSucceeds(howManyFailures = 6)
|
288
285
|
push must equalTo(1)
|
@@ -290,7 +287,7 @@ class S3WebsiteSpec extends Specification {
|
|
290
287
|
}
|
291
288
|
|
292
289
|
"s3_website.yml file" should {
|
293
|
-
"never be uploaded" in new
|
290
|
+
"never be uploaded" in new BasicSetup {
|
294
291
|
setLocalFile("s3_website.yml")
|
295
292
|
push
|
296
293
|
noUploadsOccurred must beTrue
|
@@ -298,7 +295,7 @@ class S3WebsiteSpec extends Specification {
|
|
298
295
|
}
|
299
296
|
|
300
297
|
".env file" should { // The .env file is the https://github.com/bkeepers/dotenv file
|
301
|
-
"never be uploaded" in new
|
298
|
+
"never be uploaded" in new BasicSetup {
|
302
299
|
setLocalFile(".env")
|
303
300
|
push
|
304
301
|
noUploadsOccurred must beTrue
|
@@ -306,7 +303,7 @@ class S3WebsiteSpec extends Specification {
|
|
306
303
|
}
|
307
304
|
|
308
305
|
"exclude_from_upload: string" should {
|
309
|
-
"result in matching files not being uploaded" in new
|
306
|
+
"result in matching files not being uploaded" in new BasicSetup {
|
310
307
|
config = "exclude_from_upload: .DS_.*?"
|
311
308
|
setLocalFile(".DS_Store")
|
312
309
|
push
|
@@ -319,7 +316,7 @@ class S3WebsiteSpec extends Specification {
|
|
319
316
|
- regex
|
320
317
|
- another_exclusion
|
321
318
|
""" should {
|
322
|
-
"result in matching files not being uploaded" in new
|
319
|
+
"result in matching files not being uploaded" in new BasicSetup {
|
323
320
|
config = """
|
324
321
|
|exclude_from_upload:
|
325
322
|
| - .DS_.*?
|
@@ -332,14 +329,14 @@ class S3WebsiteSpec extends Specification {
|
|
332
329
|
}
|
333
330
|
|
334
331
|
"ignore_on_server: value" should {
|
335
|
-
"not delete the S3 objects that match the ignore value" in new
|
332
|
+
"not delete the S3 objects that match the ignore value" in new BasicSetup {
|
336
333
|
config = "ignore_on_server: logs"
|
337
334
|
setS3Files(S3File("logs/log.txt", ""))
|
338
335
|
push
|
339
336
|
noDeletesOccurred must beTrue
|
340
337
|
}
|
341
338
|
|
342
|
-
"support non-US-ASCII files" in new
|
339
|
+
"support non-US-ASCII files" in new BasicSetup {
|
343
340
|
setS3Files(S3File("tags/笔记/test.html", ""))
|
344
341
|
config = "ignore_on_server: tags/笔记/test.html"
|
345
342
|
push
|
@@ -352,7 +349,7 @@ class S3WebsiteSpec extends Specification {
|
|
352
349
|
- regex
|
353
350
|
- another_ignore
|
354
351
|
""" should {
|
355
|
-
"not delete the S3 objects that match the ignore value" in new
|
352
|
+
"not delete the S3 objects that match the ignore value" in new BasicSetup {
|
356
353
|
config = """
|
357
354
|
|ignore_on_server:
|
358
355
|
| - .*txt
|
@@ -362,7 +359,7 @@ class S3WebsiteSpec extends Specification {
|
|
362
359
|
noDeletesOccurred must beTrue
|
363
360
|
}
|
364
361
|
|
365
|
-
"support non-US-ASCII files"
|
362
|
+
"support non-US-ASCII files" in new BasicSetup {
|
366
363
|
setS3Files(S3File("tags/笔记/test.html", ""))
|
367
364
|
config = """
|
368
365
|
|ignore_on_server:
|
@@ -373,15 +370,51 @@ class S3WebsiteSpec extends Specification {
|
|
373
370
|
}
|
374
371
|
}
|
375
372
|
|
373
|
+
"site in config" should {
|
374
|
+
"let the user deploy a site from a custom location" in new CustomSiteDirectory with EmptySite with MockAWS with DefaultRunMode {
|
375
|
+
config = s"site: $siteDirectory"
|
376
|
+
setLocalFile(".vimrc")
|
377
|
+
|
378
|
+
new File(siteDirectory, ".vimrc").exists() must beTrue // Sanity check
|
379
|
+
siteDirectory must not equalTo workingDirectory // Sanity check
|
380
|
+
|
381
|
+
push
|
382
|
+
sentPutObjectRequest.getKey must equalTo(".vimrc")
|
383
|
+
}
|
384
|
+
|
385
|
+
"not override the --site command-line switch" in new BasicSetup {
|
386
|
+
config = s"site: dir-that-does-not-exist"
|
387
|
+
setLocalFile(".vimrc") // This creates a file in the directory into which the --site CLI arg points
|
388
|
+
push
|
389
|
+
sentPutObjectRequest.getKey must equalTo(".vimrc")
|
390
|
+
}
|
391
|
+
|
392
|
+
automaticallySupportedSiteGenerators foreach { siteGenerator =>
|
393
|
+
"override an automatically detected site" in new CustomSiteDirectory with EmptySite with MockAWS with DefaultRunMode {
|
394
|
+
addContentToAutomaticallyDetectedSite(workingDirectory)
|
395
|
+
config = s"site: $siteDirectory"
|
396
|
+
setLocalFile(".vimrc") // Add content to the custom site directory
|
397
|
+
push
|
398
|
+
sentPutObjectRequest.getKey must equalTo(".vimrc")
|
399
|
+
}
|
400
|
+
|
401
|
+
def addContentToAutomaticallyDetectedSite(workingDirectory: File) {
|
402
|
+
val automaticallyDetectedSiteDir = new File(workingDirectory, siteGenerator.outputDirectory)
|
403
|
+
automaticallyDetectedSiteDir.mkdirs()
|
404
|
+
write(new File(automaticallyDetectedSiteDir, ".bashrc"), "echo hello")
|
405
|
+
}
|
406
|
+
}
|
407
|
+
}
|
408
|
+
|
376
409
|
"max-age in config" can {
|
377
|
-
"be applied to all files" in new
|
410
|
+
"be applied to all files" in new BasicSetup {
|
378
411
|
config = "max_age: 60"
|
379
412
|
setLocalFile("index.html")
|
380
413
|
push
|
381
414
|
sentPutObjectRequest.getMetadata.getCacheControl must equalTo("max-age=60")
|
382
415
|
}
|
383
416
|
|
384
|
-
"be applied to files that match the glob" in new
|
417
|
+
"be applied to files that match the glob" in new BasicSetup {
|
385
418
|
config = """
|
386
419
|
|max_age:
|
387
420
|
| "*.html": 90
|
@@ -391,7 +424,7 @@ class S3WebsiteSpec extends Specification {
|
|
391
424
|
sentPutObjectRequest.getMetadata.getCacheControl must equalTo("max-age=90")
|
392
425
|
}
|
393
426
|
|
394
|
-
"be applied to directories that match the glob" in new
|
427
|
+
"be applied to directories that match the glob" in new BasicSetup {
|
395
428
|
config = """
|
396
429
|
|max_age:
|
397
430
|
| "assets/**/*.js": 90
|
@@ -401,7 +434,7 @@ class S3WebsiteSpec extends Specification {
|
|
401
434
|
sentPutObjectRequest.getMetadata.getCacheControl must equalTo("max-age=90")
|
402
435
|
}
|
403
436
|
|
404
|
-
"not be applied if the glob doesn't match" in new
|
437
|
+
"not be applied if the glob doesn't match" in new BasicSetup {
|
405
438
|
config = """
|
406
439
|
|max_age:
|
407
440
|
| "*.js": 90
|
@@ -411,14 +444,14 @@ class S3WebsiteSpec extends Specification {
|
|
411
444
|
sentPutObjectRequest.getMetadata.getCacheControl must beNull
|
412
445
|
}
|
413
446
|
|
414
|
-
"be used to disable caching" in new
|
447
|
+
"be used to disable caching" in new BasicSetup {
|
415
448
|
config = "max_age: 0"
|
416
449
|
setLocalFile("index.html")
|
417
450
|
push
|
418
451
|
sentPutObjectRequest.getMetadata.getCacheControl must equalTo("no-cache; max-age=0")
|
419
452
|
}
|
420
453
|
|
421
|
-
"support non-US-ASCII directory names" in new
|
454
|
+
"support non-US-ASCII directory names" in new BasicSetup {
|
422
455
|
config = """
|
423
456
|
|max_age:
|
424
457
|
| "*": 21600
|
@@ -429,7 +462,7 @@ class S3WebsiteSpec extends Specification {
|
|
429
462
|
}
|
430
463
|
|
431
464
|
"max-age in config" should {
|
432
|
-
"respect the more specific glob" in new
|
465
|
+
"respect the more specific glob" in new BasicSetup {
|
433
466
|
config = """
|
434
467
|
|max_age:
|
435
468
|
| "assets/*": 150
|
@@ -443,7 +476,7 @@ class S3WebsiteSpec extends Specification {
|
|
443
476
|
}
|
444
477
|
|
445
478
|
"s3_reduced_redundancy: true in config" should {
|
446
|
-
"result in uploads being marked with reduced redundancy" in new
|
479
|
+
"result in uploads being marked with reduced redundancy" in new BasicSetup {
|
447
480
|
config = "s3_reduced_redundancy: true"
|
448
481
|
setLocalFile("file.exe")
|
449
482
|
push
|
@@ -452,7 +485,7 @@ class S3WebsiteSpec extends Specification {
|
|
452
485
|
}
|
453
486
|
|
454
487
|
"s3_reduced_redundancy: false in config" should {
|
455
|
-
"result in uploads being marked with the default storage class" in new
|
488
|
+
"result in uploads being marked with the default storage class" in new BasicSetup {
|
456
489
|
config = "s3_reduced_redundancy: false"
|
457
490
|
setLocalFile("file.exe")
|
458
491
|
push
|
@@ -461,7 +494,7 @@ class S3WebsiteSpec extends Specification {
|
|
461
494
|
}
|
462
495
|
|
463
496
|
"redirect in config" should {
|
464
|
-
"result in a redirect instruction that is sent to AWS" in new
|
497
|
+
"result in a redirect instruction that is sent to AWS" in new BasicSetup {
|
465
498
|
config = """
|
466
499
|
|redirects:
|
467
500
|
| index.php: /index.html
|
@@ -470,7 +503,7 @@ class S3WebsiteSpec extends Specification {
|
|
470
503
|
sentPutObjectRequest.getRedirectLocation must equalTo("/index.html")
|
471
504
|
}
|
472
505
|
|
473
|
-
"add slash to the redirect target" in new
|
506
|
+
"add slash to the redirect target" in new BasicSetup {
|
474
507
|
config = """
|
475
508
|
|redirects:
|
476
509
|
| index.php: index.html
|
@@ -479,7 +512,7 @@ class S3WebsiteSpec extends Specification {
|
|
479
512
|
sentPutObjectRequest.getRedirectLocation must equalTo("/index.html")
|
480
513
|
}
|
481
514
|
|
482
|
-
"support external redirects" in new
|
515
|
+
"support external redirects" in new BasicSetup {
|
483
516
|
config = """
|
484
517
|
|redirects:
|
485
518
|
| index.php: http://www.youtube.com/watch?v=dQw4w9WgXcQ
|
@@ -488,7 +521,7 @@ class S3WebsiteSpec extends Specification {
|
|
488
521
|
sentPutObjectRequest.getRedirectLocation must equalTo("http://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
489
522
|
}
|
490
523
|
|
491
|
-
"support external redirects that point to an HTTPS target" in new
|
524
|
+
"support external redirects that point to an HTTPS target" in new BasicSetup {
|
492
525
|
config = """
|
493
526
|
|redirects:
|
494
527
|
| index.php: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
@@ -497,7 +530,7 @@ class S3WebsiteSpec extends Specification {
|
|
497
530
|
sentPutObjectRequest.getRedirectLocation must equalTo("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
498
531
|
}
|
499
532
|
|
500
|
-
"result in max-age=0 Cache-Control header on the object" in new
|
533
|
+
"result in max-age=0 Cache-Control header on the object" in new BasicSetup {
|
501
534
|
config = """
|
502
535
|
|redirects:
|
503
536
|
| index.php: /index.html
|
@@ -508,7 +541,7 @@ class S3WebsiteSpec extends Specification {
|
|
508
541
|
}
|
509
542
|
|
510
543
|
"redirect in config and an object on the S3 bucket" should {
|
511
|
-
"not result in the S3 object being deleted" in new
|
544
|
+
"not result in the S3 object being deleted" in new BasicSetup {
|
512
545
|
config = """
|
513
546
|
|redirects:
|
514
547
|
| index.php: /index.html
|
@@ -521,7 +554,7 @@ class S3WebsiteSpec extends Specification {
|
|
521
554
|
}
|
522
555
|
|
523
556
|
"dotfiles" should {
|
524
|
-
"be included in the pushed files" in new
|
557
|
+
"be included in the pushed files" in new BasicSetup {
|
525
558
|
setLocalFile(".vimrc")
|
526
559
|
push
|
527
560
|
sentPutObjectRequest.getKey must equalTo(".vimrc")
|
@@ -529,25 +562,25 @@ class S3WebsiteSpec extends Specification {
|
|
529
562
|
}
|
530
563
|
|
531
564
|
"content type inference" should {
|
532
|
-
"add charset=utf-8 to all html documents" in new
|
565
|
+
"add charset=utf-8 to all html documents" in new BasicSetup {
|
533
566
|
setLocalFile("index.html")
|
534
567
|
push
|
535
568
|
sentPutObjectRequest.getMetadata.getContentType must equalTo("text/html; charset=utf-8")
|
536
569
|
}
|
537
570
|
|
538
|
-
"add charset=utf-8 to all text documents" in new
|
571
|
+
"add charset=utf-8 to all text documents" in new BasicSetup {
|
539
572
|
setLocalFile("index.txt")
|
540
573
|
push
|
541
574
|
sentPutObjectRequest.getMetadata.getContentType must equalTo("text/plain; charset=utf-8")
|
542
575
|
}
|
543
576
|
|
544
|
-
"add charset=utf-8 to all json documents" in new
|
577
|
+
"add charset=utf-8 to all json documents" in new BasicSetup {
|
545
578
|
setLocalFile("data.json")
|
546
579
|
push
|
547
580
|
sentPutObjectRequest.getMetadata.getContentType must equalTo("application/json; charset=utf-8")
|
548
581
|
}
|
549
582
|
|
550
|
-
"resolve the content type from file contents" in new
|
583
|
+
"resolve the content type from file contents" in new BasicSetup {
|
551
584
|
setLocalFileWithContent(("index", "<html><body><h1>hi</h1></body></html>"))
|
552
585
|
push
|
553
586
|
sentPutObjectRequest.getMetadata.getContentType must equalTo("text/html; charset=utf-8")
|
@@ -555,7 +588,7 @@ class S3WebsiteSpec extends Specification {
|
|
555
588
|
}
|
556
589
|
|
557
590
|
"ERB in config file" should {
|
558
|
-
"be evaluated" in new
|
591
|
+
"be evaluated" in new BasicSetup {
|
559
592
|
config = """
|
560
593
|
|redirects:
|
561
594
|
|<%= ('a'..'f').to_a.map do |t| ' '+t+ ': /'+t+'.html' end.join('\n')%>
|
@@ -567,14 +600,14 @@ class S3WebsiteSpec extends Specification {
|
|
567
600
|
}
|
568
601
|
|
569
602
|
"dry run" should {
|
570
|
-
"not push updates" in new
|
603
|
+
"not push updates" in new SiteLocationFromCliArg with EmptySite with MockAWS with DryRunMode {
|
571
604
|
setLocalFileWithContent(("index.html", "<div>new</div>"))
|
572
605
|
setS3Files(S3File("index.html", md5Hex("<div>old</div>")))
|
573
606
|
push
|
574
607
|
noUploadsOccurred must beTrue
|
575
608
|
}
|
576
609
|
|
577
|
-
"not push redirects" in new
|
610
|
+
"not push redirects" in new SiteLocationFromCliArg with EmptySite with MockAWS with DryRunMode {
|
578
611
|
config =
|
579
612
|
"""
|
580
613
|
|redirects:
|
@@ -584,19 +617,19 @@ class S3WebsiteSpec extends Specification {
|
|
584
617
|
noUploadsOccurred must beTrue
|
585
618
|
}
|
586
619
|
|
587
|
-
"not push deletes" in new
|
620
|
+
"not push deletes" in new SiteLocationFromCliArg with EmptySite with MockAWS with DryRunMode {
|
588
621
|
setS3Files(S3File("index.html", md5Hex("<div>old</div>")))
|
589
622
|
push
|
590
623
|
noUploadsOccurred must beTrue
|
591
624
|
}
|
592
625
|
|
593
|
-
"not push new files" in new
|
626
|
+
"not push new files" in new SiteLocationFromCliArg with EmptySite with MockAWS with DryRunMode {
|
594
627
|
setLocalFile("index.html")
|
595
628
|
push
|
596
629
|
noUploadsOccurred must beTrue
|
597
630
|
}
|
598
|
-
|
599
|
-
"not invalidate files" in new
|
631
|
+
|
632
|
+
"not invalidate files" in new SiteLocationFromCliArg with EmptySite with MockAWS with DryRunMode {
|
600
633
|
config = "cloudfront_invalidation_id: AABBCC"
|
601
634
|
setS3Files(S3File("index.html", md5Hex("<div>old</div>")))
|
602
635
|
push
|
@@ -619,6 +652,8 @@ class S3WebsiteSpec extends Specification {
|
|
619
652
|
sentPutObjectRequests.length must equalTo(1)
|
620
653
|
}
|
621
654
|
}
|
655
|
+
|
656
|
+
trait BasicSetup extends SiteLocationFromCliArg with EmptySite with MockAWS with DefaultRunMode
|
622
657
|
|
623
658
|
trait DefaultRunMode {
|
624
659
|
implicit def pushMode: PushMode = new PushMode {
|
@@ -633,7 +668,7 @@ class S3WebsiteSpec extends Specification {
|
|
633
668
|
}
|
634
669
|
|
635
670
|
trait MockAWS extends MockS3 with MockCloudFront with Scope
|
636
|
-
|
671
|
+
|
637
672
|
trait MockCloudFront extends MockAWSHelper {
|
638
673
|
val amazonCloudFrontClient = mock(classOf[AmazonCloudFront])
|
639
674
|
implicit val cfSettings: CloudFrontSetting = CloudFrontSetting(
|
@@ -677,7 +712,7 @@ class S3WebsiteSpec extends Specification {
|
|
677
712
|
when(amazonCloudFrontClient.createInvalidation(Matchers.anyObject())).thenThrow(new AmazonServiceException("CloudFront is down"))
|
678
713
|
}
|
679
714
|
}
|
680
|
-
|
715
|
+
|
681
716
|
trait MockS3 extends MockAWSHelper {
|
682
717
|
val amazonS3Client = mock(classOf[AmazonS3])
|
683
718
|
implicit val s3Settings: S3Setting = S3Setting(
|
@@ -754,7 +789,7 @@ class S3WebsiteSpec extends Specification {
|
|
754
789
|
true // Mockito is based on exceptions
|
755
790
|
}
|
756
791
|
|
757
|
-
type S3Key = String
|
792
|
+
type S3Key = String
|
758
793
|
}
|
759
794
|
|
760
795
|
trait MockAWSHelper {
|
@@ -770,8 +805,10 @@ class S3WebsiteSpec extends Specification {
|
|
770
805
|
}
|
771
806
|
|
772
807
|
trait Directories extends BeforeAfter {
|
773
|
-
|
774
|
-
val
|
808
|
+
def randomDir() = new File(FileUtils.getTempDirectory, "s3_website_dir" + Random.nextLong())
|
809
|
+
implicit final val workingDirectory: File = randomDir()
|
810
|
+
implicit def yamlConfig: S3_website_yml = S3_website_yml(new File(workingDirectory, "s3_website.yml"))
|
811
|
+
val siteDirectory: File
|
775
812
|
val configDirectory: File = workingDirectory // Represents the --config-dir=X option
|
776
813
|
|
777
814
|
def before {
|
@@ -785,22 +822,28 @@ class S3WebsiteSpec extends Specification {
|
|
785
822
|
}
|
786
823
|
}
|
787
824
|
|
788
|
-
|
789
|
-
* Represents the situation where the current working directory, site dir and config dir are in the same directory
|
790
|
-
*/
|
791
|
-
trait AllInSameDirectory extends Directories {
|
825
|
+
trait SiteLocationFromCliArg extends Directories {
|
792
826
|
val siteDirectory = workingDirectory
|
827
|
+
val siteDirFromCLIArg = true
|
793
828
|
}
|
794
829
|
|
795
830
|
trait JekyllSite extends Directories {
|
796
831
|
val siteDirectory = new File(workingDirectory, "_site")
|
832
|
+
val siteDirFromCLIArg = false
|
797
833
|
}
|
798
834
|
|
799
835
|
trait NanocSite extends Directories {
|
800
836
|
val siteDirectory = new File(workingDirectory, "public/output")
|
837
|
+
val siteDirFromCLIArg = false
|
801
838
|
}
|
802
|
-
|
839
|
+
|
840
|
+
trait CustomSiteDirectory extends Directories {
|
841
|
+
val siteDirectory = randomDir()
|
842
|
+
val siteDirFromCLIArg = false
|
843
|
+
}
|
844
|
+
|
803
845
|
trait EmptySite extends Directories {
|
846
|
+
val siteDirFromCLIArg: Boolean
|
804
847
|
type LocalFileWithContent = (String, String)
|
805
848
|
|
806
849
|
def setLocalFile(fileName: String) = setLocalFileWithContent((fileName, ""))
|
@@ -812,6 +855,7 @@ class S3WebsiteSpec extends Specification {
|
|
812
855
|
file.createNewFile()
|
813
856
|
write(file, fileNameAndContent._2)
|
814
857
|
}
|
858
|
+
|
815
859
|
var config = ""
|
816
860
|
val baseConfig =
|
817
861
|
"""
|
@@ -820,34 +864,38 @@ class S3WebsiteSpec extends Specification {
|
|
820
864
|
|s3_bucket: bucket
|
821
865
|
""".stripMargin
|
822
866
|
|
823
|
-
implicit def
|
824
|
-
|
825
|
-
|
826
|
-
private def buildCliArgs(
|
827
|
-
config: String = "",
|
828
|
-
baseConfig: String =
|
829
|
-
"""
|
830
|
-
|s3_id: foo
|
831
|
-
|s3_secret: bar
|
832
|
-
|s3_bucket: bucket
|
833
|
-
""".stripMargin
|
834
|
-
): CliArgs = {
|
835
|
-
val configFile = new File(configDirectory, "s3_website.yml")
|
836
|
-
write(configFile,
|
867
|
+
implicit def configString: ConfigString =
|
868
|
+
ConfigString(
|
837
869
|
s"""
|
838
870
|
|$baseConfig
|
839
871
|
|$config
|
840
872
|
""".stripMargin
|
841
873
|
)
|
874
|
+
|
875
|
+
def pushMode: PushMode // Represents the --dry-run switch
|
876
|
+
|
877
|
+
implicit def cliArgs: CliArgs =
|
842
878
|
new CliArgs {
|
843
|
-
|
879
|
+
def verbose = true
|
844
880
|
|
845
|
-
|
881
|
+
def dryRun = pushMode.dryRun
|
846
882
|
|
847
|
-
|
883
|
+
def site = if (siteDirFromCLIArg) siteDirectory.getAbsolutePath else null
|
848
884
|
|
849
|
-
|
885
|
+
def configDir = configDirectory.getAbsolutePath
|
850
886
|
}
|
851
|
-
}
|
852
887
|
}
|
888
|
+
|
889
|
+
def push(implicit
|
890
|
+
emptyYamlConfig: S3_website_yml,
|
891
|
+
configString: ConfigString,
|
892
|
+
cliArgs: CliArgs,
|
893
|
+
s3Settings: S3Setting,
|
894
|
+
cloudFrontSettings: CloudFrontSetting,
|
895
|
+
workingDirectory: File) = {
|
896
|
+
write(emptyYamlConfig.file, configString.yaml) // Write the yaml config lazily, so that the tests can override the default yaml config
|
897
|
+
Push.push
|
898
|
+
}
|
899
|
+
|
900
|
+
case class ConfigString(yaml: String)
|
853
901
|
}
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: s3_website
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.2.0
|
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-07-
|
11
|
+
date: 2014-07-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -126,8 +126,8 @@ files:
|
|
126
126
|
- resources/s3_website.jar.md5
|
127
127
|
- s3_website.gemspec
|
128
128
|
- sbt
|
129
|
-
- src/main/java/s3/website/ByteHelper.java
|
130
129
|
- src/main/resources/log4j.properties
|
130
|
+
- src/main/scala/s3/website/ByteHelper.scala
|
131
131
|
- src/main/scala/s3/website/CloudFront.scala
|
132
132
|
- src/main/scala/s3/website/Diff.scala
|
133
133
|
- src/main/scala/s3/website/Logger.scala
|
@@ -1,14 +0,0 @@
|
|
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
|
-
}
|