@heliosgraphics/epoque 0.0.1-alpha.1 → 0.0.1-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/sync.ts +46 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heliosgraphics/epoque",
3
- "version": "0.0.1-alpha.1",
3
+ "version": "0.0.1-alpha.2",
4
4
  "author": "Chris Puska <chris@puska.org>",
5
5
  "description": "sync a local folder",
6
6
  "type": "module",
package/src/sync.ts CHANGED
@@ -107,7 +107,7 @@ const getLocalById = (articles: Array<LocalArticle>): Map<string, LocalArticle>
107
107
  const getRemoteById = (archive: RemoteArchive): Map<string, RemoteArticle> =>
108
108
  new Map(archive.articles.map((article) => [article.id, article]))
109
109
 
110
- const assertNoUpstreamChanges = ({
110
+ const getUpstreamConflicts = ({
111
111
  archive,
112
112
  config,
113
113
  localArticles,
@@ -115,7 +115,7 @@ const assertNoUpstreamChanges = ({
115
115
  archive: RemoteArchive
116
116
  config: ProjectConfig
117
117
  localArticles: Array<LocalArticle>
118
- }): void => {
118
+ }): Array<string> => {
119
119
  const conflicts: Array<string> = []
120
120
  const localById = getLocalById(localArticles)
121
121
 
@@ -147,9 +147,24 @@ const assertNoUpstreamChanges = ({
147
147
  }
148
148
  }
149
149
 
150
- if (conflicts.length) {
151
- throw new Error(`sync aborted because upstream is newer:\n${conflicts.map((conflict) => `- ${conflict}`).join("\n")}`)
152
- }
150
+ return conflicts
151
+ }
152
+
153
+ const formatUpstreamConflicts = (conflicts: Array<string>): string =>
154
+ `remote has newer changes:\n${conflicts.map((conflict) => `- ${conflict}`).join("\n")}`
155
+
156
+ const isConfirmed = (value: string): boolean => ["y", "yes"].includes(value.trim().toLowerCase())
157
+
158
+ const confirmUpstreamOverride = async ({ dryRun, prompt }: SyncOptions, conflicts: Array<string>): Promise<boolean> => {
159
+ if (!conflicts.length) return false
160
+
161
+ const question = dryRun
162
+ ? `${formatUpstreamConflicts(conflicts)}\nContinue dry run using local folder as source of truth? [y/N] `
163
+ : `${formatUpstreamConflicts(conflicts)}\nOverride remote changes with local folder? [y/N] `
164
+
165
+ if (isConfirmed(await prompt(question))) return true
166
+
167
+ throw new Error(`sync aborted because upstream is newer`)
153
168
  }
154
169
 
155
170
  const getUpdateBody = (article: LocalArticle): Record<string, unknown> => ({
@@ -176,18 +191,32 @@ const syncAttachments = async ({
176
191
  api,
177
192
  article,
178
193
  dryRun,
194
+ overwriteUpstream,
195
+ remoteArticle,
179
196
  state,
180
197
  summary,
181
198
  }: {
182
199
  api: EpoqueApi
183
200
  article: LocalArticle
184
201
  dryRun: boolean
202
+ overwriteUpstream: boolean
203
+ remoteArticle: RemoteArticle | undefined
185
204
  state: ArticleSyncState
186
205
  summary: SyncSummary
187
206
  }): Promise<ArticleSyncState> => {
188
207
  const nextState: ArticleSyncState = { ...state, attachments: {} }
189
208
  const localPaths = new Set(article.attachments.map((attachment) => attachment.relativePath))
190
209
  const imageIds: Array<string> = []
210
+ const trackedImageIds = new Set(Object.values(state.attachments).map((attachment) => attachment.id))
211
+
212
+ if (overwriteUpstream && remoteArticle) {
213
+ for (const image of remoteArticle.images) {
214
+ if (trackedImageIds.has(image.id)) continue
215
+
216
+ summary.deletedAttachments += 1
217
+ if (!dryRun) await api.deleteAttachment(image.url)
218
+ }
219
+ }
191
220
 
192
221
  for (const attachment of article.attachments) {
193
222
  const existing = state.attachments[attachment.relativePath]
@@ -236,14 +265,20 @@ export const syncFolder = async (options: SyncOptions): Promise<SyncSummary> =>
236
265
  const remoteById = getRemoteById(archive)
237
266
  const localById = getLocalById(localArticles)
238
267
  const summary: SyncSummary = { ...EMPTY_SUMMARY }
268
+ const overwriteUpstream = await confirmUpstreamOverride(options, getUpstreamConflicts({ archive, config, localArticles }))
239
269
 
240
- assertNoUpstreamChanges({ archive, config, localArticles })
270
+ for (const remoteArticle of archive.articles) {
271
+ if (localById.has(remoteArticle.id)) continue
272
+ if (!overwriteUpstream && !config.articles[remoteArticle.id]) continue
273
+
274
+ summary.deletedArticles += 1
275
+ if (!options.dryRun) await options.api.deleteArticle(remoteArticle.id)
276
+ delete config.articles[remoteArticle.id]
277
+ }
241
278
 
242
279
  for (const articleId of Object.keys(config.articles)) {
243
- if (localById.has(articleId) || !remoteById.has(articleId)) continue
280
+ if (localById.has(articleId) || remoteById.has(articleId)) continue
244
281
 
245
- summary.deletedArticles += 1
246
- if (!options.dryRun) await options.api.deleteArticle(articleId)
247
282
  delete config.articles[articleId]
248
283
  }
249
284
 
@@ -281,6 +316,8 @@ export const syncFolder = async (options: SyncOptions): Promise<SyncSummary> =>
281
316
  api: options.api,
282
317
  article: localArticle,
283
318
  dryRun: options.dryRun,
319
+ overwriteUpstream,
320
+ remoteArticle,
284
321
  state: { ...state, file: localArticle.file, remoteUpdatedAt: remoteArticle?.updatedAt ?? state.remoteUpdatedAt },
285
322
  summary,
286
323
  })