Skip to content

Commit 60e2f93

Browse files
author
CI Fix
committed
missing content in ldp.mjs
1 parent f3d0a1b commit 60e2f93

File tree

4 files changed

+277
-8
lines changed

4 files changed

+277
-8
lines changed

lib/ldp.mjs

Lines changed: 210 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
11
/* eslint-disable node/no-deprecated-api */
22

33
import { join, dirname } from 'path'
4-
import { createRequire } from 'module'
5-
const require = createRequire(import.meta.url)
6-
const intoStream = require('into-stream')
4+
import intoStream from 'into-stream'
75
import url from 'url'
86
import fs from 'fs'
97
import $rdf from 'rdflib'
10-
const { mkdirp } = require('fs-extra')
8+
import { mkdirp } from 'fs-extra'
119
import { v4 as uuid } from 'uuid' // there seem to be an esm module
1210
import debug from './debug.mjs'
1311
import error from './http-error.mjs'
1412
import { stringToStream, serialize, overQuota, getContentType, parse } from './utils.mjs'
15-
const extend = require('extend')
16-
const rimraf = require('rimraf')
13+
import extend from 'extend'
14+
import rimraf from 'rimraf'
15+
import { exec } from 'child_process'
1716
import * as ldpContainer from './ldp-container.mjs'
1817
import fetch from 'node-fetch'
1918
import { promisify } from 'util'
20-
import URL from 'url'
2119
import withLock from './lock.mjs'
2220
import utilPath from 'path'
2321
import { clearAclCache } from './acl-checker.mjs'
@@ -413,7 +411,6 @@ class LDP {
413411
} else {
414412
// If original is a folder, copy recursively
415413
copyPromise = new Promise((resolve, reject) => {
416-
const { exec } = require('child_process')
417414
exec(`cp -r "${fromPath}" "${toPath}"`, function (err) {
418415
if (err) {
419416
debug.handlers('Error copying directory: ' + err)
@@ -598,9 +595,214 @@ class LDP {
598595
}
599596
}
600597

598+
async graph (url, baseUri, contentType) {
599+
const body = await this.readResource(url)
600+
if (!contentType) {
601+
({ contentType } = await this.resourceMapper.mapUrlToFile({ url }))
602+
}
603+
return new Promise((resolve, reject) => {
604+
const graph = $rdf.graph()
605+
$rdf.parse(body, graph, baseUri, contentType,
606+
err => err ? reject(err) : resolve(graph))
607+
})
608+
}
609+
601610
static getRDFMimeTypes () {
602611
return Array.from(RDF_MIME_TYPES)
603612
}
613+
614+
getTrustedOrigins (req) {
615+
const trustedOrigins = [this.resourceMapper.resolveUrl(req.hostname)].concat(this.trustedOrigins)
616+
if (this.multiuser) {
617+
trustedOrigins.push(this.serverUri)
618+
}
619+
return trustedOrigins
620+
}
621+
622+
async getAvailableUrl (hostname, containerURI, { slug = uuid(), extension, container } = {}) {
623+
let requestUrl = this.resourceMapper.resolveUrl(hostname, containerURI)
624+
requestUrl = requestUrl.replace(/\/*$/, '/')
625+
626+
let itemName = slug.endsWith(extension) || slug.endsWith(this.suffixAcl) || slug.endsWith(this.suffixMeta) ? slug : slug + extension
627+
try {
628+
// check whether resource exists
629+
const context = container ? '/' : ''
630+
await this.resourceMapper.mapUrlToFile({ url: (requestUrl + itemName + context) })
631+
itemName = `${uuid()}-${itemName}`
632+
} catch (e) {
633+
try {
634+
// check whether resource with same name exists
635+
const context = !container ? '/' : ''
636+
await this.resourceMapper.mapUrlToFile({ url: (requestUrl + itemName + context) })
637+
itemName = `${uuid()}-${itemName}`
638+
} catch (e) {}
639+
}
640+
if (container) itemName += '/'
641+
return requestUrl + itemName
642+
}
643+
644+
async exists (hostname, path, searchIndex = true) {
645+
const options = { hostname, path, includeBody: false, searchIndex }
646+
return await this.get(options, searchIndex)
647+
}
648+
649+
async fetchGraph (uri, options = {}) {
650+
const response = await fetch(uri)
651+
if (!response.ok) {
652+
const err = new Error(
653+
`Error fetching ${uri}: ${response.status} ${response.statusText}`
654+
)
655+
err.statusCode = response.status || 400
656+
throw err
657+
}
658+
const body = await response.text()
659+
660+
return parse(body, uri, getContentType(response.headers))
661+
}
662+
663+
async checkItemName (url) {
664+
let testName, testPath
665+
const { hostname, pathname } = this.resourceMapper._parseUrl(url)
666+
let itemUrl = this.resourceMapper.resolveUrl(hostname, pathname)
667+
if (this._containsInvalidSuffixes(itemUrl)) {
668+
throw error(400, `${itemUrl} contained reserved suffixes in path`)
669+
}
670+
const container = itemUrl.endsWith('/')
671+
try {
672+
const testUrl = container ? itemUrl.slice(0, -1) : itemUrl + '/'
673+
const { path: testPathLocal } = await this.resourceMapper.mapUrlToFile({ url: testUrl })
674+
testPath = testPathLocal
675+
testName = container ? fs.lstatSync(testPath).isFile() : fs.lstatSync(testPath).isDirectory()
676+
} catch (err) {
677+
testName = false
678+
679+
if (itemUrl.endsWith('/')) itemUrl = itemUrl.substring(0, itemUrl.length - 1)
680+
itemUrl = itemUrl.substring(0, itemUrl.lastIndexOf('/') + 1)
681+
const { pathname: parentPathname } = this.resourceMapper._parseUrl(itemUrl)
682+
if (parentPathname !== '/') {
683+
return await this.checkItemName(itemUrl)
684+
}
685+
}
686+
if (testName) {
687+
throw error(409, `${testPath}: Container and resource cannot have the same name in URI`)
688+
}
689+
}
690+
691+
async createDirectory (pathArg, hostname, nonContainer = true) {
692+
try {
693+
const dirName = dirname(pathArg)
694+
if (!fs.existsSync(dirName)) {
695+
await promisify(mkdirp)(dirName)
696+
if (this.live && nonContainer) {
697+
const parentDirectoryPath = utilPath.dirname(dirName) + utilPath.sep
698+
const parentDirectoryUrl = (await this.resourceMapper.mapFileToUrl({ path: parentDirectoryPath, hostname })).url
699+
this.live(url.parse(parentDirectoryUrl).pathname)
700+
}
701+
}
702+
} catch (err) {
703+
debug.handlers('PUT -- Error creating directory: ' + err)
704+
throw error(err, 'Failed to create the path to the new resource')
705+
}
706+
}
707+
708+
async checkFileExtension (urlArg, pathArg) {
709+
try {
710+
const { path: existingPath } = await this.resourceMapper.mapUrlToFile({ url: urlArg })
711+
if (pathArg !== existingPath) {
712+
try {
713+
await withLock(existingPath, () => promisify(fs.unlink)(existingPath))
714+
} catch (err) { throw error(err, 'Failed to delete resource') }
715+
}
716+
} catch (err) { }
717+
}
718+
719+
async deleteContainer (directory) {
720+
if (directory[directory.length - 1] !== '/') directory += '/'
721+
let list
722+
try {
723+
list = await promisify(fs.readdir)(directory)
724+
} catch (err) {
725+
throw error(404, 'The container does not exist')
726+
}
727+
if (list.some(file => !file.endsWith(this.suffixMeta) && !file.endsWith(this.suffixAcl))) {
728+
throw error(409, 'Container is not empty')
729+
}
730+
try {
731+
await promisify(rimraf)(directory)
732+
} catch (err) {
733+
throw error(err, 'Failed to delete the container')
734+
}
735+
}
736+
737+
async deleteDocument (filePath) {
738+
const linkPath = this.resourceMapper._removeDollarExtension(filePath)
739+
try {
740+
await withLock(filePath, () => promisify(fs.unlink)(filePath))
741+
const aclPath = `${linkPath}${this.suffixAcl}`
742+
if (await promisify(fs.exists)(aclPath)) {
743+
await withLock(aclPath, () => promisify(fs.unlink)(aclPath))
744+
}
745+
const metaPath = `${linkPath}${this.suffixMeta}`
746+
if (await promisify(fs.exists)(metaPath)) {
747+
await withLock(metaPath, () => promisify(fs.unlink)(metaPath))
748+
}
749+
} catch (err) {
750+
debug.container('DELETE -- unlink() error: ' + err)
751+
throw error(err, 'Failed to delete resource')
752+
}
753+
}
754+
755+
async get (options, searchIndex = true) {
756+
let pathLocal, contentType, stats
757+
try {
758+
({ path: pathLocal, contentType } = await this.resourceMapper.mapUrlToFile({ url: options, searchIndex }))
759+
stats = await this.stat(pathLocal)
760+
} catch (err) {
761+
throw error(err.status || 500, err.message)
762+
}
763+
764+
if (!options.includeBody) {
765+
return { stream: stats, contentType, container: stats.isDirectory() }
766+
}
767+
768+
if (stats.isDirectory()) {
769+
const { url: absContainerUri } = await this.resourceMapper.mapFileToUrl({ path: pathLocal, hostname: options.hostname })
770+
const metaFile = await this.readContainerMeta(absContainerUri).catch(() => '')
771+
let data
772+
try {
773+
data = await this.listContainer(pathLocal, absContainerUri, metaFile, options.hostname)
774+
} catch (err) {
775+
debug.handlers('GET container -- Read error:' + err.message)
776+
throw err
777+
}
778+
const stream = stringToStream(data)
779+
return { stream, contentType, container: true }
780+
} else {
781+
let chunksize, contentRange, start, end
782+
if (options.range) {
783+
const total = fs.statSync(pathLocal).size
784+
const parts = options.range.replace(/bytes=/, '').split('-')
785+
const partialstart = parts[0]
786+
const partialend = parts[1]
787+
start = parseInt(partialstart, 10)
788+
end = partialend ? parseInt(partialend, 10) : total - 1
789+
chunksize = (end - start) + 1
790+
contentRange = 'bytes ' + start + '-' + end + '/' + total
791+
}
792+
return withLock(pathLocal, () => new Promise((resolve, reject) => {
793+
const stream = fs.createReadStream(pathLocal, start && end ? { start, end } : {})
794+
stream
795+
.on('error', function (err) {
796+
debug.handlers(`GET -- error reading ${pathLocal}: ${err.message}`)
797+
return reject(error(err, "Can't read file " + err))
798+
})
799+
.on('open', function () {
800+
debug.handlers(`GET -- Reading ${pathLocal}`)
801+
return resolve({ stream, contentType, container: false, contentRange, chunksize })
802+
})
803+
}))
804+
}
805+
}
604806
}
605807

606808
export default LDP
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { strict as assert } from 'assert'
2+
import LDP from '../../lib/ldp.mjs'
3+
4+
export async function test_noExistingResource() {
5+
const rm = {
6+
resolveUrl: (hostname, containerURI) => `https://${hostname}/root${containerURI}/`,
7+
mapUrlToFile: async () => { throw new Error('Not found') }
8+
}
9+
const ldp = new LDP({ resourceMapper: rm })
10+
const url = await ldp.getAvailableUrl('host.test', '/container', { slug: 'name.txt', extension: '', container: false })
11+
assert.equal(url, 'https://host.test/root/container/name.txt')
12+
}
13+
14+
export async function test_existingResourcePrefixes() {
15+
let called = 0
16+
const rm = {
17+
resolveUrl: (hostname, containerURI) => `https://${hostname}/root${containerURI}/`,
18+
mapUrlToFile: async () => {
19+
called += 1
20+
// First call indicates file exists (resolve), so return some object
21+
if (called === 1) return { path: '/some/path' }
22+
// Subsequent calls simulate not found
23+
throw new Error('Not found')
24+
}
25+
}
26+
const ldp = new LDP({ resourceMapper: rm })
27+
const url = await ldp.getAvailableUrl('host.test', '/container', { slug: 'name.txt', extension: '', container: false })
28+
// Should contain a uuid-prefix before name.txt, i.e. -name.txt
29+
assert.ok(url.endsWith('-name.txt') || url.includes('-name.txt'))
30+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { describe, it } from 'mocha'
2+
import { assert } from 'chai'
3+
import LDP from '../../lib/ldp.mjs'
4+
5+
describe('LDP.getTrustedOrigins', () => {
6+
it('includes resourceMapper.resolveUrl(hostname), trustedOrigins and serverUri when multiuser', () => {
7+
const rm = { resolveUrl: (hostname) => `https://${hostname}/` }
8+
const ldp = new LDP({ resourceMapper: rm, trustedOrigins: ['https://trusted.example/'], multiuser: true, serverUri: 'https://server.example/' })
9+
const res = ldp.getTrustedOrigins({ hostname: 'host.test' })
10+
assert.includeMembers(res, ['https://host.test/', 'https://trusted.example/', 'https://server.example/'])
11+
})
12+
13+
it('omits serverUri when not multiuser', () => {
14+
const rm = { resolveUrl: (hostname) => `https://${hostname}/` }
15+
const ldp = new LDP({ resourceMapper: rm, trustedOrigins: ['https://trusted.example/'], multiuser: false, serverUri: 'https://server.example/' })
16+
const res = ldp.getTrustedOrigins({ hostname: 'host.test' })
17+
assert.includeMembers(res, ['https://host.test/', 'https://trusted.example/'])
18+
assert.notInclude(res, 'https://server.example/')
19+
})
20+
})

test-esm/unit/ldp-api-test.mjs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { describe, it } from 'mocha'
2+
import { assert } from 'chai'
3+
import LDP from '../../lib/ldp.mjs'
4+
5+
describe('LDP ESM API', () => {
6+
it('exports expected methods', () => {
7+
const proto = LDP.prototype
8+
const expected = [
9+
'stat', 'readResource', 'readContainerMeta', 'listContainer', 'post', 'put', 'putResource', 'putValidateData',
10+
'delete', 'copy', 'patch', 'applyPatch', 'applyPatchUpdate', 'applyPatchInsertDelete', 'parseQuads',
11+
'getGraph', 'graph', 'getAvailableUrl', 'getTrustedOrigins', 'exists', 'get'
12+
]
13+
expected.forEach(fn => {
14+
assert.strictEqual(typeof proto[fn], 'function', `Missing method ${fn}`)
15+
})
16+
})
17+
})

0 commit comments

Comments
 (0)