Skip to content

Commit 930eccb

Browse files
committed
Added checking last modified times for copy operations to skip newer target resources
1 parent f49a8c5 commit 930eccb

File tree

3 files changed

+85
-14
lines changed

3 files changed

+85
-14
lines changed

src/commands/solid-copy.ts

+55-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from 'fs'
22
import path from 'path'
33
import { getFile, getContentType, createContainerAt } from "@inrupt/solid-client"
4-
import { isRemote, isDirectory, FileInfo, ensureDirectoryExistence, fixLocalPath, readRemoteDirectoryRecursively, checkRemoteFileExists, writeErrorString, isDirectoryContents, resourceExists } from '../utils/util';
4+
import { isRemote, isDirectory, FileInfo, ensureDirectoryExistence, fixLocalPath, readRemoteDirectoryRecursively, writeErrorString, isDirectoryContents, resourceExists, getLocalFileLastModified, getRemoteResourceLastModified, compareLastModifiedTimes } from '../utils/util';
55
import Blob from 'fetch-blob'
66
import { requestUserCLIConfirmationDefaultNegative } from '../utils/userInteractions';
77
import BashlibError from '../utils/errors/BashlibError';
@@ -20,10 +20,23 @@ interface SourceOptions {
2020
isDir: boolean
2121
}
2222

23+
interface FileRetrieval {
24+
buffer: Buffer,
25+
contentType: string,
26+
lastModified?: Date
27+
}
28+
29+
interface ResourceRetrieval {
30+
blob: any,
31+
contentType: string,
32+
lastModified?: Date
33+
}
34+
2335
export interface ICommandOptionsCopy extends ICommandOptions {
2436
all?: boolean,
2537
override?: boolean,
2638
neverOverride?: boolean,
39+
compareLastModified?: boolean,
2740
}
2841

2942
export default async function copy(src: string, dst: string, options?: ICommandOptionsCopy) : Promise<{
@@ -35,6 +48,7 @@ export default async function copy(src: string, dst: string, options?: ICommandO
3548
commandOptions.all = commandOptions.all || false;
3649
commandOptions.override = commandOptions.override || false;
3750
commandOptions.neverOverride = commandOptions.neverOverride || false;
51+
commandOptions.compareLastModified = commandOptions.compareLastModified || false; // todo: fix
3852

3953
/**************************
4054
* Preprocess src and dst *
@@ -193,47 +207,53 @@ export default async function copy(src: string, dst: string, options?: ICommandO
193207
* UTILITY FUNCTIONS *
194208
*********************/
195209

196-
async function getLocalSourceFiles(source: SourceOptions, verbose: boolean, all: boolean, options?: { logger?: Logger }): Promise<{files: FileInfo[], directories: FileInfo[], aclfiles: FileInfo[]}> {
210+
async function getLocalSourceFiles(source: SourceOptions, verbose: boolean, all: boolean, options?: { logger?: Logger, compareLastModified?: boolean }): Promise<{files: FileInfo[], directories: FileInfo[], aclfiles: FileInfo[]}> {
197211
if (source.isDir) {
198212
let filePathInfos = readLocalDirectoryRecursively(source.path, undefined, {verbose, all} )
199213
let files = await Promise.all(filePathInfos.files.map(async fileInfo => {
200214
fileInfo.loadFile = async () => readLocalFile(fileInfo.absolutePath, verbose, options)
215+
fileInfo.lastModified = options?.compareLastModified ? getLocalFileLastModified(source.path) : undefined;
201216
return fileInfo
202217
}))
203218
let aclfiles = await Promise.all(filePathInfos.aclfiles.map(async fileInfo => {
219+
fileInfo.lastModified = options?.compareLastModified ? getLocalFileLastModified(source.path) : undefined;
204220
fileInfo.loadFile = async () => readLocalFile(fileInfo.absolutePath, verbose, options)
205221
return fileInfo
206222
}))
207223
return { files, aclfiles, directories: filePathInfos.directories }
208224
} else {
209225
return { files: [ {
226+
lastModified: options?.compareLastModified ? getLocalFileLastModified(source.path) : undefined,
210227
absolutePath: source.path,
211228
relativePath: '',
212229
loadFile: async () => readLocalFile(source.path, verbose, options)
213230
} ], aclfiles: [], directories: [] }
214231
}
215232
}
216233

217-
async function getRemoteSourceFiles(source: SourceOptions, fetch: typeof globalThis.fetch, verbose: boolean, all: boolean, options?: { logger?: Logger }) : Promise<{files: FileInfo[], directories: FileInfo[], aclfiles: FileInfo[]}> {
234+
async function getRemoteSourceFiles(source: SourceOptions, fetch: typeof globalThis.fetch, verbose: boolean, all: boolean, options?: { logger?: Logger, compareLastModified?: boolean }) : Promise<{files: FileInfo[], directories: FileInfo[], aclfiles: FileInfo[]}> {
218235
if (source.isDir) {
219236
let discoveredResources = await readRemoteDirectoryRecursively(source.path, { fetch, verbose, all})
220237

221238
// Filter out files that return errors (e.g no authentication privileges)
222239
let files = (await Promise.all(discoveredResources.files.map(async fileInfo => {
223240
fileInfo.loadFile = async () => readRemoteFile(fileInfo.absolutePath, fetch, verbose, options)
241+
fileInfo.lastModified = await (options?.compareLastModified ? getRemoteResourceLastModified(source.path, fetch) : undefined)
224242
return fileInfo
225243
}))).filter(f => f) as FileInfo[]
226244

227245
let aclfiles : FileInfo[] = []
228246
if (all) {
229247
aclfiles = (await Promise.all(discoveredResources.aclfiles.map(async fileInfo => {
230248
fileInfo.loadFile = async () => readRemoteFile(fileInfo.absolutePath, fetch, verbose, options)
249+
fileInfo.lastModified = await (options?.compareLastModified ? getRemoteResourceLastModified(source.path, fetch) : undefined)
231250
return fileInfo
232251
}))).filter(f => f) as FileInfo[]
233252
}
234253
return { files, aclfiles, directories: discoveredResources.directories }
235254
} else {
236255
return { files: [ {
256+
lastModified: await (options?.compareLastModified ? getRemoteResourceLastModified(source.path, fetch) : undefined),
237257
absolutePath: source.path,
238258
relativePath: '',
239259
loadFile: async () => readRemoteFile(source.path, fetch, verbose, options)
@@ -242,19 +262,28 @@ async function getRemoteSourceFiles(source: SourceOptions, fetch: typeof globalT
242262

243263
}
244264

245-
function readLocalFile(path: string, verbose: boolean, options?: { logger?: Logger }): { buffer: Buffer, contentType: string} {
265+
function readLocalFile(path: string, verbose: boolean, options?: { logger?: Logger, compareLastModified?: boolean }): FileRetrieval {
246266
if (verbose) (options?.logger || console).log('Reading local file:', path)
247267
const file = fs.readFileSync(path)
248-
let contentType = path.endsWith('.acl') || path.endsWith('.meta') ? 'text/turtle': path.endsWith('.acp') ? 'application/ld+json': mime.lookup(path)
249-
return { buffer: file, contentType };
268+
const contentType = path.endsWith('.acl') || path.endsWith('.meta') ? 'text/turtle': path.endsWith('.acp') ? 'application/ld+json': mime.lookup(path)
269+
// if (options?.compareLastModified) {
270+
// const lastModified = getLocalFileLastModified(path)
271+
// return { buffer: file, contentType, lastModified };
272+
// } else {
273+
return { buffer: file, contentType };
274+
// }
250275
}
251276

252-
async function readRemoteFile(path: string, fetch: any, verbose: boolean, options?: { logger?: Logger }) : Promise<{ blob: any, contentType: string}> {
277+
async function readRemoteFile(path: string, fetch: any, verbose: boolean, options?: { logger?: Logger, compareLastModified?: boolean }) : Promise< ResourceRetrieval > {
253278
if (verbose) (options?.logger || console).log('Reading remote file:', path)
254-
const file = await getFile(path, { fetch })
255-
const contentType = await getContentType(file) as string // TODO:: error handling?
256-
return { blob: file as any, contentType };
257-
279+
const resourceFile = await getFile(path, { fetch })
280+
const contentType = await getContentType(resourceFile) as string // TODO:: error handling?
281+
// if (options?.compareLastModified) {
282+
// const lastModified = await getRemoteResourceLastModified(path, fetch)
283+
// return { blob: resourceFile as any, contentType, lastModified };
284+
// } else {
285+
return { blob: resourceFile as any, contentType };
286+
// }
258287
}
259288

260289
async function writeLocalDirectory(path: string, fileInfo: FileInfo, options: ICommandOptionsCopy): Promise<any> {
@@ -276,6 +305,11 @@ async function writeLocalFile(resourcePath: string, fileInfo: FileInfo, options:
276305
ensureDirectoryExistence(resourcePath);
277306

278307
let executeWrite = true
308+
if (options.compareLastModified) {
309+
const targetResourceLastModified = await getLocalFileLastModified(resourcePath)
310+
const decision = await compareLastModifiedTimes(fileInfo.lastModified, targetResourceLastModified)
311+
executeWrite = decision.write
312+
}
279313
if (options.neverOverride || !options.override) {
280314
if (await resourceExists(resourcePath, options.fetch)) {
281315
if (options.neverOverride) {
@@ -285,6 +319,7 @@ async function writeLocalFile(resourcePath: string, fileInfo: FileInfo, options:
285319
}
286320
}
287321
}
322+
288323
if (!executeWrite) {
289324
if (options.verbose) (options.logger || console).log('Skipping existing local file:', resourcePath)
290325
return undefined;
@@ -325,7 +360,14 @@ async function writeLocalFile(resourcePath: string, fileInfo: FileInfo, options:
325360
async function writeRemoteFile(resourcePath: string, fileInfo: FileInfo, fetch: any, options: ICommandOptionsCopy): Promise<string | undefined> {
326361
resourcePath = resourcePath.split('$.')[0];
327362
let executeWrite = true
328-
if (options.neverOverride || !options.override) {
363+
let executeRequest = true;
364+
if (options.compareLastModified) {
365+
const targetResourceLastModified = await getRemoteResourceLastModified(resourcePath, options.fetch)
366+
const decision = await compareLastModifiedTimes(fileInfo.lastModified, targetResourceLastModified)
367+
executeWrite = decision.write
368+
executeRequest = decision.request
369+
}
370+
if (!executeWrite && executeRequest && (options.neverOverride || !options.override)) {
329371
if (await resourceExists(resourcePath, fetch)) {
330372
if (options.neverOverride) {
331373
executeWrite = false;
@@ -336,7 +378,7 @@ async function writeRemoteFile(resourcePath: string, fileInfo: FileInfo, fetch:
336378
}
337379

338380
if (!executeWrite) {
339-
if (options.verbose) (options.logger || console).log('Skipping existing local file:', resourcePath)
381+
if (options.verbose) (options.logger || console).log('Skipping existing remote file:', resourcePath)
340382
return undefined;
341383
}
342384

src/shell/commands/copy.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default class CopyCommand extends SolidCommand {
2020
.option('-o, --override', 'Automatically override existing files')
2121
.option('-n, --never-override', 'Automatically override existing files')
2222
.option('-v, --verbose', 'Log all read and write operations')
23+
.option('-c, --compare-last-modified', 'Skip targets with newer "last-modified" status')
2324
.action(this.executeCommand)
2425

2526
return program

src/utils/util.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { getSolidDataset, getContainedResourceUrlAll, getUrl, getUrlAll, getThing, getThingAll, getDatetime, getInteger, SolidDataset, acp_ess_2, hasAccessibleAcl, FetchError } from '@inrupt/solid-client';
22
import { requestUserIdp } from './userInteractions';
33
import type { Logger } from '../logger';
4+
import * as fs from "fs"
45

5-
const fs = require('fs')
66
const path = require('path')
77
var LinkHeader = require( 'http-link-header' )
88
const mime = require('mime-types');
@@ -27,6 +27,7 @@ export type FileInfo = {
2727
directory?: string,
2828
contentType?: string,
2929
buffer?: Buffer,
30+
lastModified?: Date,
3031
blob?: Blob,
3132
loadFile?: FileLoadingFunction
3233
}
@@ -536,4 +537,31 @@ export async function resourceExists(url: string, fetch: any) {
536537
return undefined;
537538
}
538539
}
540+
}
541+
542+
543+
export async function compareLastModifiedTimes(sourceResourceTime: Date | undefined, targetResourceTime: Date | undefined ): Promise<{
544+
write: boolean, request: boolean
545+
}> {
546+
if (!targetResourceTime || !sourceResourceTime) return { write: false, request: true }
547+
else if ( targetResourceTime < sourceResourceTime ) return { write: true, request: false }
548+
else return { write: false, request: false }
549+
}
550+
551+
552+
export async function getRemoteResourceLastModified(url: string, fetch: any): Promise< Date | undefined > {
553+
try {
554+
let res = await fetch(url, { method: "HEAD" })
555+
if (!res.ok) return undefined
556+
const lastModified = res.headers.get('last-modified')
557+
return new Date(lastModified)
558+
}
559+
catch (e) {
560+
return undefined
561+
}
562+
}
563+
564+
565+
export function getLocalFileLastModified(path: string): Date {
566+
return fs.statSync(path).mtime
539567
}

0 commit comments

Comments
 (0)