s3_website 2.1.6 → 2.1.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/changelog.md +6 -3
- data/lib/s3_website/version.rb +1 -1
- data/resources/s3_website.jar.md5 +1 -1
- data/src/main/scala/s3/website/Diff.scala +13 -254
- data/src/main/scala/s3/website/Push.scala +42 -49
- data/src/main/scala/s3/website/S3.scala +3 -11
- data/src/main/scala/s3/website/model/push.scala +1 -1
- data/src/test/scala/s3/website/S3WebsiteSpec.scala +1 -61
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8a50d9894a8f340bff511731fe4934a413e324a2
|
4
|
+
data.tar.gz: 23008693b34025381965ff5721796c78d63a5b72
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 98f1cb95e395dc00cc47f71af25cd2ac40e75e08dd7299e89df85d403e0179abbfcec2ae1c0b85caf9b1235fcd9a4fee25116d7bc7a6d511209ff8bb3504c6ba
|
7
|
+
data.tar.gz: 396c0a1d1a4f9a45baa6365a0041955d169b391384c243267092faa9a57be3f1d65d0ed8a7d1fc555b63ad5f07d19416219f3dc4968c5102c52aa1c04dbb701f
|
data/changelog.md
CHANGED
@@ -2,6 +2,12 @@
|
|
2
2
|
|
3
3
|
This project uses [Semantic Versioning](http://semver.org).
|
4
4
|
|
5
|
+
## 2.1.7
|
6
|
+
|
7
|
+
* Remove local db
|
8
|
+
|
9
|
+
It turned out to be too complex to maintain
|
10
|
+
|
5
11
|
## 2.1.6
|
6
12
|
|
7
13
|
* Automatically add slash to redirects if needed
|
@@ -40,9 +46,6 @@ This project uses [Semantic Versioning](http://semver.org).
|
|
40
46
|
|
41
47
|
* Faster uploads for extra large sites
|
42
48
|
|
43
|
-
Use a local database for calculating diffs. This removes the need to read all
|
44
|
-
the files of the website, when you call the `s3_website push` command.
|
45
|
-
|
46
49
|
Use proper multithreading with JVM threads.
|
47
50
|
|
48
51
|
* Simulate deployments with `push --dry-run`
|
data/lib/s3_website/version.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
a0ae3ccc855ccb2b3488da03422f492a
|
@@ -5,30 +5,18 @@ import s3.website.Ruby.rubyRegexMatches
|
|
5
5
|
import scala.concurrent.{ExecutionContextExecutor, Future}
|
6
6
|
import scala.util.{Failure, Success, Try}
|
7
7
|
import java.io.File
|
8
|
-
import org.apache.commons.io.FileUtils._
|
9
|
-
import org.apache.commons.codec.digest.DigestUtils._
|
10
|
-
import scala.io.Source
|
11
|
-
import s3.website.Diff.LocalFileDatabase.resolveDiffAgainstLocalDb
|
12
|
-
import s3.website.Diff.UploadBatch
|
13
|
-
|
14
|
-
case class Diff(
|
15
|
-
unchanged: Future[Either[ErrorReport, Seq[S3Key]]],
|
16
|
-
uploads: Seq[UploadBatch],
|
17
|
-
persistenceError: Future[Option[ErrorReport]]
|
18
|
-
)
|
19
8
|
|
20
9
|
object Diff {
|
21
10
|
|
22
|
-
type
|
11
|
+
type FutureUploads = Future[Either[ErrorReport, Seq[Upload]]]
|
23
12
|
|
24
13
|
def resolveDiff(s3FilesFuture: Future[Either[ErrorReport, Seq[S3File]]])
|
25
|
-
(implicit site: Site, logger: Logger, executor: ExecutionContextExecutor):
|
26
|
-
|
27
|
-
else resolveDiffAgainstGetBucketResponse(s3FilesFuture)
|
14
|
+
(implicit site: Site, logger: Logger, executor: ExecutionContextExecutor): FutureUploads =
|
15
|
+
resolveDiffAgainstGetBucketResponse(s3FilesFuture)
|
28
16
|
|
29
17
|
private def resolveDiffAgainstGetBucketResponse(s3FilesFuture: Future[Either[ErrorReport, Seq[S3File]]])
|
30
|
-
(implicit site: Site, logger: Logger, executor: ExecutionContextExecutor):
|
31
|
-
|
18
|
+
(implicit site: Site, logger: Logger, executor: ExecutionContextExecutor): FutureUploads =
|
19
|
+
s3FilesFuture.map { errorOrS3Files =>
|
32
20
|
errorOrS3Files.right.flatMap { s3Files =>
|
33
21
|
Try {
|
34
22
|
val s3KeyIndex = s3Files.map(_.s3Key).toSet
|
@@ -37,62 +25,30 @@ object Diff {
|
|
37
25
|
val existsOnS3 = (f: File) => s3KeyIndex contains site.resolveS3Key(f)
|
38
26
|
val isChangedOnS3 = (upload: Upload) => !(s3Md5Index contains upload.md5.get)
|
39
27
|
val newUploads = siteFiles collect {
|
40
|
-
case file if !existsOnS3(file) => Upload(file, NewFile
|
28
|
+
case file if !existsOnS3(file) => Upload(file, NewFile)
|
41
29
|
}
|
42
30
|
val changedUploads = siteFiles collect {
|
43
|
-
case file if existsOnS3(file) => Upload(file, FileUpdate
|
31
|
+
case file if existsOnS3(file) => Upload(file, FileUpdate)
|
44
32
|
} filter isChangedOnS3
|
45
|
-
|
46
|
-
val newOrChangedFiles = (changedUploads ++ newUploads).map(_.originalFile).toSet
|
47
|
-
siteFiles.filterNot(f => newOrChangedFiles contains f)
|
48
|
-
}
|
49
|
-
val recordsAndUploads: Seq[Either[DbRecord, Upload]] = unchangedFiles.map {
|
50
|
-
f => Left(DbRecord(f))
|
51
|
-
} ++ (changedUploads ++ newUploads).map {
|
52
|
-
Right(_)
|
53
|
-
}
|
54
|
-
LocalFileDatabase persist recordsAndUploads
|
55
|
-
recordsAndUploads
|
33
|
+
newUploads ++ changedUploads
|
56
34
|
} match {
|
57
35
|
case Success(ok) => Right(ok)
|
58
36
|
case Failure(err) => Left(ErrorReport(err))
|
59
37
|
}
|
60
38
|
}
|
61
39
|
}
|
62
|
-
def collectResult[B](pf: PartialFunction[Either[DbRecord, Upload],B]) =
|
63
|
-
diffAgainstS3.map { errorOrDiffSource =>
|
64
|
-
errorOrDiffSource.right map (_ collect pf)
|
65
|
-
}
|
66
|
-
val unchanged = collectResult {
|
67
|
-
case Left(dbRecord) => dbRecord.s3Key
|
68
|
-
}
|
69
|
-
val uploads: UploadBatch = collectResult {
|
70
|
-
case Right(upload) => upload
|
71
|
-
}
|
72
|
-
Right(Diff(unchanged, uploads :: Nil, persistenceError = Future(None)))
|
73
|
-
}
|
74
40
|
|
75
|
-
def resolveDeletes(
|
76
|
-
(implicit
|
77
|
-
val
|
78
|
-
errorOrUnchanged <- diff.unchanged
|
79
|
-
errorsOrChanges <- Future.sequence(diff.uploads)
|
80
|
-
} yield
|
81
|
-
errorsOrChanges.foldLeft(errorOrUnchanged: Either[ErrorReport, Seq[S3Key]]) { (memo, errorOrChanges) =>
|
82
|
-
for {
|
83
|
-
mem <- memo.right
|
84
|
-
keysToDelete <- errorOrChanges.right
|
85
|
-
} yield mem ++ keysToDelete.map(_.s3Key)
|
86
|
-
}
|
41
|
+
def resolveDeletes(s3Files: Future[Either[ErrorReport, Seq[S3File]]], redirects: Seq[Redirect])
|
42
|
+
(implicit site: Site, logger: Logger, executor: ExecutionContextExecutor): Future[Either[ErrorReport, Seq[S3Key]]] = {
|
43
|
+
val localS3Keys = Files.listSiteFiles.map(site resolveS3Key)
|
87
44
|
|
88
|
-
s3Files
|
45
|
+
s3Files map { s3Files: Either[ErrorReport, Seq[S3File]] =>
|
89
46
|
for {
|
90
|
-
localS3Keys <- errorOrLocalKeys.right
|
91
47
|
remoteS3Keys <- s3Files.right.map(_ map (_.s3Key)).right
|
92
48
|
} yield {
|
93
49
|
val keysToRetain = (localS3Keys ++ (redirects map { _.s3Key })).toSet
|
94
50
|
remoteS3Keys filterNot { s3Key =>
|
95
|
-
val ignoreOnServer = config.ignore_on_server.exists(_.fold(
|
51
|
+
val ignoreOnServer = site.config.ignore_on_server.exists(_.fold(
|
96
52
|
(ignoreRegex: String) => rubyRegexMatches(s3Key, ignoreRegex),
|
97
53
|
(ignoreRegexes: Seq[String]) => ignoreRegexes.exists(rubyRegexMatches(s3Key, _))
|
98
54
|
))
|
@@ -100,203 +56,6 @@ object Diff {
|
|
100
56
|
(keysToRetain contains s3Key) || ignoreOnServer
|
101
57
|
}
|
102
58
|
}
|
103
|
-
}.tupled
|
104
|
-
}
|
105
|
-
|
106
|
-
object LocalFileDatabase {
|
107
|
-
def hasRecords(implicit site: Site, logger: Logger) =
|
108
|
-
(for {
|
109
|
-
dbFile <- getOrCreateDbFile
|
110
|
-
databaseIndices <- loadDbFromFile(dbFile)
|
111
|
-
} yield databaseIndices.fullIndex.headOption.isDefined) getOrElse false
|
112
|
-
|
113
|
-
def resolveDiffAgainstLocalDb(s3FilesFuture: Future[Either[ErrorReport, Seq[S3File]]])
|
114
|
-
(implicit site: Site, logger: Logger, executor: ExecutionContextExecutor): Either[ErrorReport, Diff] = {
|
115
|
-
val localDiff: Either[ErrorReport, Seq[Either[DbRecord, Upload]]] =
|
116
|
-
(for {
|
117
|
-
dbFile <- getOrCreateDbFile
|
118
|
-
databaseIndices <- loadDbFromFile(dbFile)
|
119
|
-
} yield {
|
120
|
-
val siteFiles = Files.listSiteFiles
|
121
|
-
val recordsOrUploads = siteFiles.foldLeft(Seq(): Seq[Either[DbRecord, Upload]]) { (recordsOrUps, file) =>
|
122
|
-
val truncatedKey = TruncatedDbRecord(file)
|
123
|
-
val fileIsUnchanged = databaseIndices.truncatedIndex contains truncatedKey
|
124
|
-
if (fileIsUnchanged)
|
125
|
-
recordsOrUps :+ Left(databaseIndices.fullIndex find (_.truncated == truncatedKey) get)
|
126
|
-
else {
|
127
|
-
val isUpdate = databaseIndices.s3KeyIndex contains truncatedKey.s3Key
|
128
|
-
|
129
|
-
val uploadType =
|
130
|
-
if (isUpdate) FileUpdate
|
131
|
-
else NewFile
|
132
|
-
recordsOrUps :+ Right(Upload(file, uploadType, reasonForUpload(truncatedKey, databaseIndices, isUpdate)))
|
133
|
-
}
|
134
|
-
}
|
135
|
-
logger.debug(s"Discovered ${siteFiles.length} files on the local site, of which ${recordsOrUploads count (_.isRight)} are new or changed")
|
136
|
-
recordsOrUploads
|
137
|
-
}) match {
|
138
|
-
case Success(ok) => Right(ok)
|
139
|
-
case Failure(err) => Left(ErrorReport(err))
|
140
|
-
}
|
141
|
-
|
142
|
-
localDiff.right map { localDiffResult =>
|
143
|
-
val unchangedAccordingToLocalDiff = localDiffResult collect {
|
144
|
-
case Left(f) => f
|
145
|
-
}
|
146
|
-
|
147
|
-
val uploadsAccordingToLocalDiff = localDiffResult collect {
|
148
|
-
case Right(f) => f
|
149
|
-
}
|
150
|
-
|
151
|
-
val changesMissedByLocalDiff: Future[Either[ErrorReport, Seq[Upload]]] = s3FilesFuture.map { errorOrS3Files =>
|
152
|
-
for (s3Files <- errorOrS3Files.right) yield {
|
153
|
-
val remoteS3Keys = s3Files.map(_.s3Key).toSet
|
154
|
-
val localS3Keys = unchangedAccordingToLocalDiff.map(_.s3Key).toSet
|
155
|
-
val localMd5 = unchangedAccordingToLocalDiff.map(_.uploadFileMd5).toSet
|
156
|
-
def isChangedOnS3(s3File: S3File) = (localS3Keys contains s3File.s3Key) && !(localMd5 contains s3File.md5)
|
157
|
-
val changedOnS3 = s3Files collect {
|
158
|
-
case s3File if isChangedOnS3(s3File) =>
|
159
|
-
Upload(site resolveFile s3File, FileUpdate, reasonForUpload = "someone else has modified the file on the S3 bucket")
|
160
|
-
}
|
161
|
-
val missingFromS3 = localS3Keys collect {
|
162
|
-
case localS3Key if !(remoteS3Keys contains localS3Key) =>
|
163
|
-
Upload(site resolveFile localS3Key, NewFile, reasonForUpload = "someone else has removed the file from the S3 bucket")
|
164
|
-
|
165
|
-
}
|
166
|
-
changedOnS3 ++ missingFromS3
|
167
|
-
}
|
168
|
-
}
|
169
|
-
|
170
|
-
val errorOrDiffAgainstS3 =
|
171
|
-
changesMissedByLocalDiff map { errorOrUploads =>
|
172
|
-
errorOrUploads.right map { uploadsMissedByLocalDiff =>
|
173
|
-
val uploadsS3KeyIndex = uploadsMissedByLocalDiff.map(_.s3Key).toSet
|
174
|
-
val unchangedAccordingToLocalAndS3Diff = unchangedAccordingToLocalDiff.filterNot(uploadsS3KeyIndex contains _.s3Key)
|
175
|
-
(unchangedAccordingToLocalAndS3Diff, uploadsMissedByLocalDiff)
|
176
|
-
}
|
177
|
-
}
|
178
|
-
|
179
|
-
val unchangedFilesFinal = errorOrDiffAgainstS3 map {
|
180
|
-
_ fold (
|
181
|
-
(error: ErrorReport) => Left(error),
|
182
|
-
(syncResult: (Seq[DbRecord], Seq[Upload])) => Right(syncResult._1)
|
183
|
-
)
|
184
|
-
}
|
185
|
-
|
186
|
-
val uploadsAccordingToS3Diff = errorOrDiffAgainstS3.map {
|
187
|
-
_ fold (
|
188
|
-
(error: ErrorReport) => Left(error),
|
189
|
-
(syncResult: (Seq[DbRecord], Seq[Upload])) => Right(syncResult._2)
|
190
|
-
)
|
191
|
-
}
|
192
|
-
val persistenceError: Future[Either[ErrorReport, _]] = for {
|
193
|
-
unchanged <- unchangedFilesFinal
|
194
|
-
uploads <- uploadsAccordingToS3Diff
|
195
|
-
} yield
|
196
|
-
for {
|
197
|
-
records1 <- unchanged.right
|
198
|
-
records2 <- uploads.right
|
199
|
-
} yield
|
200
|
-
persist(records1.map(Left(_)) ++ records2.map(Right(_)) ++ uploadsAccordingToLocalDiff.map(Right(_))) match {
|
201
|
-
case Success(_) => Unit
|
202
|
-
case Failure(err) => ErrorReport(err)
|
203
|
-
}
|
204
|
-
Diff(
|
205
|
-
unchangedFilesFinal map (_.right.map(_ map (_.s3Key))),
|
206
|
-
uploads = Future(Right(uploadsAccordingToLocalDiff)) :: uploadsAccordingToS3Diff :: Nil,
|
207
|
-
persistenceError = persistenceError map (_.left.toOption)
|
208
|
-
)
|
209
|
-
}
|
210
59
|
}
|
211
|
-
|
212
|
-
private def reasonForUpload(truncatedKey: TruncatedDbRecord, databaseIndices: DbIndices, isUpdate: Boolean) = {
|
213
|
-
if (isUpdate) {
|
214
|
-
val lengthChanged = !(databaseIndices.fileLenghtIndex contains truncatedKey.fileLength)
|
215
|
-
val mtimeChanged = !(databaseIndices.lastModifiedIndex contains truncatedKey.fileModified)
|
216
|
-
if (mtimeChanged && lengthChanged)
|
217
|
-
"file mtime and length have changed"
|
218
|
-
else if (lengthChanged)
|
219
|
-
"file length has changed"
|
220
|
-
else if (mtimeChanged)
|
221
|
-
"file mtime has changed"
|
222
|
-
else
|
223
|
-
"programmer error: faulty logic in inferring the reason for upload"
|
224
|
-
}
|
225
|
-
else "file is new"
|
226
|
-
}
|
227
|
-
|
228
|
-
private def getOrCreateDbFile(implicit site: Site, logger: Logger) =
|
229
|
-
Try {
|
230
|
-
val dbFile = new File(getTempDirectory, "s3_website_local_db_" + sha256Hex(site.rootDirectory))
|
231
|
-
if (!dbFile.exists()) logger.debug("Creating a new database in " + dbFile.getName)
|
232
|
-
dbFile.createNewFile()
|
233
|
-
dbFile
|
234
|
-
}
|
235
|
-
|
236
|
-
case class DbIndices(
|
237
|
-
s3KeyIndex: Set[S3Key],
|
238
|
-
fileLenghtIndex: Set[Long],
|
239
|
-
lastModifiedIndex: Set[Long],
|
240
|
-
truncatedIndex: Set[TruncatedDbRecord],
|
241
|
-
fullIndex: Set[DbRecord]
|
242
|
-
)
|
243
|
-
|
244
|
-
private def loadDbFromFile(databaseFile: File)(implicit site: Site, logger: Logger): Try[DbIndices] =
|
245
|
-
Try {
|
246
|
-
// record format: "s3Key(file.path)|length(file)|mtime(file)|md5Hex(file.encoded)"
|
247
|
-
val RecordRegex = "(.*?)\\|(\\d+)\\|(\\d+)\\|([a-f0-9]{32})".r
|
248
|
-
val fullIndex = Source
|
249
|
-
.fromFile(databaseFile, "utf-8")
|
250
|
-
.getLines()
|
251
|
-
.toStream
|
252
|
-
.map {
|
253
|
-
case RecordRegex(s3Key, fileLength, fileModified, md5) =>
|
254
|
-
DbRecord(s3Key, fileLength.toLong, fileModified.toLong, md5)
|
255
|
-
}
|
256
|
-
.toSet
|
257
|
-
DbIndices(
|
258
|
-
s3KeyIndex = fullIndex map (_.s3Key),
|
259
|
-
truncatedIndex = fullIndex map (TruncatedDbRecord(_)),
|
260
|
-
fileLenghtIndex = fullIndex map (_.fileLength),
|
261
|
-
lastModifiedIndex = fullIndex map (_.fileModified),
|
262
|
-
fullIndex = fullIndex
|
263
|
-
)
|
264
|
-
}
|
265
|
-
|
266
|
-
def persist(recordsOrUploads: Seq[Either[DbRecord, Upload]])(implicit site: Site, logger: Logger): Try[Seq[Either[DbRecord, Upload]]] =
|
267
|
-
getOrCreateDbFile flatMap { dbFile =>
|
268
|
-
Try {
|
269
|
-
val dbFileContents = recordsOrUploads.map { recordOrUpload =>
|
270
|
-
val record: DbRecord = recordOrUpload fold(
|
271
|
-
record => record,
|
272
|
-
upload => DbRecord(upload.s3Key, upload.originalFile.length, upload.originalFile.lastModified, upload.md5.get)
|
273
|
-
)
|
274
|
-
record.s3Key :: record.fileLength :: record.fileModified :: record.uploadFileMd5 :: Nil mkString "|"
|
275
|
-
} mkString "\n"
|
276
|
-
|
277
|
-
write(dbFile, dbFileContents)
|
278
|
-
recordsOrUploads
|
279
|
-
}
|
280
|
-
}
|
281
|
-
}
|
282
|
-
|
283
|
-
case class TruncatedDbRecord(s3Key: String, fileLength: Long, fileModified: Long)
|
284
|
-
|
285
|
-
object TruncatedDbRecord {
|
286
|
-
def apply(dbRecord: DbRecord): TruncatedDbRecord = TruncatedDbRecord(dbRecord.s3Key, dbRecord.fileLength, dbRecord.fileModified)
|
287
|
-
|
288
|
-
def apply(file: File)(implicit site: Site): TruncatedDbRecord = TruncatedDbRecord(site resolveS3Key file, file.length, file.lastModified)
|
289
|
-
}
|
290
|
-
|
291
|
-
/**
|
292
|
-
* @param uploadFileMd5 if the file is gzipped, this checksum should be calculated on the gzipped file, not the original file
|
293
|
-
*/
|
294
|
-
case class DbRecord(s3Key: String, fileLength: Long, fileModified: Long, uploadFileMd5: MD5) {
|
295
|
-
lazy val truncated = TruncatedDbRecord(s3Key, fileLength, fileModified)
|
296
|
-
}
|
297
|
-
|
298
|
-
object DbRecord {
|
299
|
-
def apply(original: File)(implicit site: Site): DbRecord =
|
300
|
-
DbRecord(site resolveS3Key original, original.length, original.lastModified, Upload.md5(original).get)
|
301
60
|
}
|
302
61
|
}
|
@@ -86,55 +86,49 @@ object Push {
|
|
86
86
|
val s3FilesFuture = resolveS3Files()
|
87
87
|
val redirectReports: PushReports = redirects.map(S3 uploadRedirect _) map (Right(_))
|
88
88
|
|
89
|
-
val
|
90
|
-
|
89
|
+
val pushReports: Future[PushReports] = for {
|
90
|
+
errorOrUploads: Either[ErrorReport, Seq[Upload]] <- resolveDiff(s3FilesFuture)
|
91
91
|
} yield {
|
92
|
-
val
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
)
|
99
|
-
}
|
92
|
+
val uploadReports: PushReports = errorOrUploads.fold(
|
93
|
+
error => Left(error) :: Nil,
|
94
|
+
uploads => {
|
95
|
+
uploads.map(S3 uploadFile _).map(Right(_))
|
96
|
+
}
|
97
|
+
)
|
100
98
|
val deleteReports =
|
101
|
-
Await.result(resolveDeletes(
|
99
|
+
Await.result(resolveDeletes(s3FilesFuture, redirects), 1 day).right.map { keysToDelete =>
|
102
100
|
keysToDelete map (S3 delete _)
|
103
101
|
}.fold(
|
104
102
|
error => Left(error) :: Nil,
|
105
103
|
(pushResults: Seq[Future[PushErrorOrSuccess]]) => pushResults map (Right(_))
|
106
104
|
)
|
107
|
-
|
108
|
-
newOrChangedReports ++ deleteReports ++ redirectReports ++ diffErrorReport
|
105
|
+
uploadReports ++ deleteReports ++ redirectReports
|
109
106
|
}
|
110
|
-
val
|
111
|
-
val invalidationSucceeded = invalidateCloudFrontItems(
|
107
|
+
val finishedPushOps = awaitForResults(Await.result(pushReports, 1 day))
|
108
|
+
val invalidationSucceeded = invalidateCloudFrontItems(finishedPushOps)
|
112
109
|
|
113
|
-
afterPushFinished(
|
110
|
+
afterPushFinished(finishedPushOps, invalidationSucceeded)
|
114
111
|
}
|
115
112
|
|
116
113
|
def invalidateCloudFrontItems
|
117
|
-
(
|
114
|
+
(finishedPushOperations: FinishedPushOperations)
|
118
115
|
(implicit config: Config, cloudFrontSettings: CloudFrontSetting, ec: ExecutionContextExecutor, logger: Logger, pushMode: PushMode):
|
119
116
|
Option[InvalidationSucceeded] =
|
120
117
|
config.cloudfront_distribution_id.map { distributionId =>
|
121
|
-
val pushSuccessReports =
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
)
|
136
|
-
}
|
137
|
-
)
|
118
|
+
val pushSuccessReports =
|
119
|
+
finishedPushOperations.map {
|
120
|
+
ops =>
|
121
|
+
for {
|
122
|
+
failedOrSucceededPushes <- ops.right
|
123
|
+
successfulPush <- failedOrSucceededPushes.right
|
124
|
+
} yield successfulPush
|
125
|
+
}.foldLeft(Seq(): Seq[PushSuccessReport]) {
|
126
|
+
(reports, failOrSucc) =>
|
127
|
+
failOrSucc.fold(
|
128
|
+
_ => reports,
|
129
|
+
(pushSuccessReport: PushSuccessReport) => reports :+ pushSuccessReport
|
130
|
+
)
|
131
|
+
}
|
138
132
|
val invalidationResults: Seq[Either[FailedInvalidation, SuccessfulInvalidation]] =
|
139
133
|
toInvalidationBatches(pushSuccessReports) map { invalidationBatch =>
|
140
134
|
Await.result(
|
@@ -150,26 +144,25 @@ object Push {
|
|
150
144
|
|
151
145
|
type InvalidationSucceeded = Boolean
|
152
146
|
|
153
|
-
def afterPushFinished(
|
147
|
+
def afterPushFinished(finishedPushOps: FinishedPushOperations, invalidationSucceeded: Option[Boolean])
|
154
148
|
(implicit config: Config, logger: Logger, pushMode: PushMode): ExitCode = {
|
155
|
-
val
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
)
|
166
|
-
} min 1
|
167
|
-
) max invalidationSucceeded.fold(0)(allInvalidationsSucceeded =>
|
149
|
+
val pushCounts = resolvePushCounts(finishedPushOps)
|
150
|
+
logger.info(s"Summary: ${pushCountsToString(pushCounts)}")
|
151
|
+
val pushOpExitCode = finishedPushOps.foldLeft(0) { (memo, finishedUpload) =>
|
152
|
+
memo + finishedUpload.fold(
|
153
|
+
(error: ErrorReport) => 1,
|
154
|
+
(failedOrSucceededUpload: Either[PushFailureReport, PushSuccessReport]) =>
|
155
|
+
if (failedOrSucceededUpload.isLeft) 1 else 0
|
156
|
+
)
|
157
|
+
} min 1
|
158
|
+
val cloudFrontInvalidationExitCode = invalidationSucceeded.fold(0)(allInvalidationsSucceeded =>
|
168
159
|
if (allInvalidationsSucceeded) 0 else 1
|
169
160
|
)
|
170
161
|
|
162
|
+
val exitCode = (pushOpExitCode + cloudFrontInvalidationExitCode) min 1
|
163
|
+
|
171
164
|
exitCode match {
|
172
|
-
case 0 if !pushMode.dryRun &&
|
165
|
+
case 0 if !pushMode.dryRun && pushCounts.thereWasSomethingToPush =>
|
173
166
|
logger.info(s"Successfully pushed the website to http://${config.s3_bucket}.${config.s3_endpoint.s3WebsiteHostname}")
|
174
167
|
case 1 =>
|
175
168
|
logger.fail(s"Failed to push the website to http://${config.s3_bucket}.${config.s3_endpoint.s3WebsiteHostname}")
|
@@ -35,7 +35,7 @@ object S3 {
|
|
35
35
|
val report = SuccessfulUpload(
|
36
36
|
source.fold(_.s3Key, _.s3Key),
|
37
37
|
source.fold(
|
38
|
-
upload => Left(SuccessfulNewOrCreatedDetails(upload.uploadType, upload.uploadFile.get.length(), uploadDuration
|
38
|
+
upload => Left(SuccessfulNewOrCreatedDetails(upload.uploadType, upload.uploadFile.get.length(), uploadDuration)),
|
39
39
|
redirect => Right(SuccessfulRedirectDetails(redirect.uploadType, redirect.redirectTarget))
|
40
40
|
),
|
41
41
|
putObjectRequest
|
@@ -146,7 +146,7 @@ object S3 {
|
|
146
146
|
}
|
147
147
|
|
148
148
|
case class SuccessfulRedirectDetails(uploadType: UploadType, redirectTarget: String)
|
149
|
-
case class SuccessfulNewOrCreatedDetails(uploadType: UploadType, uploadSize: Long, uploadDuration: Option[Long]
|
149
|
+
case class SuccessfulNewOrCreatedDetails(uploadType: UploadType, uploadSize: Long, uploadDuration: Option[Long])
|
150
150
|
|
151
151
|
case class SuccessfulUpload(s3Key: S3Key,
|
152
152
|
details: Either[SuccessfulNewOrCreatedDetails, SuccessfulRedirectDetails],
|
@@ -167,20 +167,12 @@ object S3 {
|
|
167
167
|
md.getContentEncoding ::
|
168
168
|
putObjectRequest.getStorageClass ::
|
169
169
|
Nil map (Option(_)) // AWS SDK may return nulls
|
170
|
-
) :+ uploadSizeForHumans :+ uploadSpeedForHumans
|
170
|
+
) :+ uploadSizeForHumans :+ uploadSpeedForHumans
|
171
171
|
detailFragments.collect {
|
172
172
|
case Some(detailFragment) => detailFragment
|
173
173
|
}.mkString(" | ")
|
174
174
|
}
|
175
175
|
|
176
|
-
lazy val uploadReason =
|
177
|
-
details
|
178
|
-
.fold(uploadDetails => Some(uploadDetails.reasonForUpload), _ => None)
|
179
|
-
.collect {
|
180
|
-
case reasonForUpload if logger.verboseOutput =>
|
181
|
-
s"upload reason: $reasonForUpload"
|
182
|
-
}
|
183
|
-
|
184
176
|
lazy val uploadSize = details.fold(
|
185
177
|
newOrCreatedDetails => Some(newOrCreatedDetails.uploadSize),
|
186
178
|
redirectDetails => None
|
@@ -50,7 +50,7 @@ case object RedirectFile extends UploadType {
|
|
50
50
|
val pushAction = Redirected
|
51
51
|
}
|
52
52
|
|
53
|
-
case class Upload(originalFile: File, uploadType: UploadType
|
53
|
+
case class Upload(originalFile: File, uploadType: UploadType)(implicit site: Site) {
|
54
54
|
lazy val s3Key = site.resolveS3Key(originalFile)
|
55
55
|
|
56
56
|
lazy val encodingOnS3 = Encoding.encodingOnS3(s3Key)
|
@@ -578,65 +578,6 @@ class S3WebsiteSpec extends Specification {
|
|
578
578
|
}
|
579
579
|
}
|
580
580
|
|
581
|
-
// Because of the local database, the first and second run are implemented differently.
|
582
|
-
"pushing files for the second time" should {
|
583
|
-
"delete the S3 objects that no longer exist on the local site" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
|
584
|
-
push
|
585
|
-
setS3Files(S3File("obsolete.txt", ""))
|
586
|
-
push
|
587
|
-
sentDelete must equalTo("obsolete.txt")
|
588
|
-
}
|
589
|
-
|
590
|
-
"delete the local db record for the file if the user deletes the file" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
|
591
|
-
setLocalFileWithContent(("file.txt", "first run"))
|
592
|
-
push
|
593
|
-
setS3Files(S3File("file.txt", md5Hex("first run")))
|
594
|
-
FileUtils.deleteQuietly(new File(siteDirectory, "file.txt"))
|
595
|
-
push
|
596
|
-
FileUtils.readLines(localDatabase) must beEmpty
|
597
|
-
}
|
598
|
-
|
599
|
-
"push new files to the bucket" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
|
600
|
-
push
|
601
|
-
setLocalFile("newfile.txt")
|
602
|
-
push
|
603
|
-
sentPutObjectRequest.getKey must equalTo("newfile.txt")
|
604
|
-
}
|
605
|
-
|
606
|
-
"push locally changed files" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
|
607
|
-
setLocalFileWithContent(("file.txt", "first run"))
|
608
|
-
push
|
609
|
-
setLocalFileWithContent(("file.txt", "second run"))
|
610
|
-
push
|
611
|
-
sentPutObjectRequests.length must equalTo(2)
|
612
|
-
}
|
613
|
-
|
614
|
-
"push locally changed files only once" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
|
615
|
-
setLocalFileWithContent(("file.txt", "first run"))
|
616
|
-
push
|
617
|
-
setS3Files(S3File("file.txt", md5Hex("first run")))
|
618
|
-
setLocalFileWithContent(("file.txt", "second run"))
|
619
|
-
push
|
620
|
-
sentPutObjectRequests.length must equalTo(2)
|
621
|
-
}
|
622
|
-
|
623
|
-
"detect files that someone else has changed on the S3 bucket" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
|
624
|
-
setLocalFileWithContent(("file.txt", "first run"))
|
625
|
-
push
|
626
|
-
setOutdatedS3Keys("file.txt")
|
627
|
-
push
|
628
|
-
sentPutObjectRequests.length must equalTo(2)
|
629
|
-
}
|
630
|
-
|
631
|
-
"push locally unchanged files that are missing from S3" in new AllInSameDirectory with EmptySite with MockAWS with DefaultRunMode {
|
632
|
-
setLocalFileWithContent(("file.txt", "first run"))
|
633
|
-
push
|
634
|
-
removeAllFilesFromS3()
|
635
|
-
push
|
636
|
-
sentPutObjectRequests.length must equalTo(2) // Even though we use the local db, we should notice that someone else has deleted file.txt
|
637
|
-
}
|
638
|
-
}
|
639
|
-
|
640
581
|
"Jekyll site" should {
|
641
582
|
"be detected automatically" in new JekyllSite with EmptySite with MockAWS with DefaultRunMode {
|
642
583
|
setLocalFile("index.html")
|
@@ -806,14 +747,13 @@ class S3WebsiteSpec extends Specification {
|
|
806
747
|
implicit final val workingDirectory: File = new File(FileUtils.getTempDirectory, "s3_website_dir" + Random.nextLong())
|
807
748
|
val siteDirectory: File // Represents the --site=X option
|
808
749
|
val configDirectory: File = workingDirectory // Represents the --config-dir=X option
|
809
|
-
lazy val localDatabase: File = new File(FileUtils.getTempDirectory, "s3_website_local_db_" + sha256Hex(siteDirectory.getPath))
|
810
750
|
|
811
751
|
def before {
|
812
752
|
workingDirectory :: siteDirectory :: configDirectory :: Nil foreach forceMkdir
|
813
753
|
}
|
814
754
|
|
815
755
|
def after {
|
816
|
-
(workingDirectory :: siteDirectory :: configDirectory ::
|
756
|
+
(workingDirectory :: siteDirectory :: configDirectory :: Nil) foreach { dir =>
|
817
757
|
if (dir.exists) forceDelete(dir)
|
818
758
|
}
|
819
759
|
}
|