Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic prep #1791

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ jobs:

strategy:
matrix:
node-version: [18.x]
node-version: [22.x]
os: [ubuntu-latest]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
# extract branch name
- if: github.event_name == 'pull_request'
run: echo "BRANCH_NAME=${GITHUB_HEAD_REF}" >> $GITHUB_ENV
Expand Down Expand Up @@ -56,7 +56,7 @@ jobs:
runs-on: ubuntu-latest
steps:

- uses: actions/checkout@v2
- uses: actions/checkout@v4

- uses: olegtarasov/[email protected]
id: tagName
Expand Down
2 changes: 1 addition & 1 deletion bin/solid
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env node
#!/usr/bin/env -S node --experimental-require-module
const startCli = require('./lib/cli')
startCli()
2 changes: 1 addition & 1 deletion bin/solid.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env node
#!/usr/bin/env -S node --experimental-require-module
const startCli = require('./lib/cli')
startCli()
2 changes: 1 addition & 1 deletion docker-image/src/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:16-alpine
FROM node:22-alpine

# hadolint ignore=DL3018
RUN apk add --no-cache openssl
Expand Down
8 changes: 8 additions & 0 deletions lib/create-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ const ResourceMapper = require('./resource-mapper')
const aclCheck = require('@solid/acl-check')
const { version } = require('../package.json')

const acceptEvents = require('express-accept-events').default
const events = require('express-events-negotiate').default
const eventID = require('express-prep/event-id').default
const prep = require('express-prep').default

const corsSettings = cors({
methods: [
'OPTIONS', 'HEAD', 'GET', 'PATCH', 'POST', 'PUT', 'DELETE'
Expand Down Expand Up @@ -61,6 +66,9 @@ function createApp (argv = {}) {

const app = express()

// Add PREP support
app.use(acceptEvents, events, eventID, prep)

initAppLocals(app, argv, ldp)
initHeaders(app)
initViews(app, configPath)
Expand Down
2 changes: 2 additions & 0 deletions lib/handlers/delete.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ async function handler (req, res, next) {
try {
await ldp.delete(req)
debug('DELETE -- Ok.')
// Add event-id for notifications
res.setHeader('Event-ID', res.setEventID())
res.sendStatus(200)
next()
} catch (err) {
Expand Down
49 changes: 42 additions & 7 deletions lib/handlers/get.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const translate = require('../utils.js').translate
const error = require('../http-error')

const RDFs = require('../ldp').mimeTypesAsArray()
const isRdf = require('../ldp').mimeTypeIsRdf

const prepConfig = 'accept=("message/rfc822" "application/ld+json" "text/turtle")'

async function handler (req, res, next) {
const ldp = req.app.locals.ldp
Expand Down Expand Up @@ -103,22 +106,46 @@ async function handler (req, res, next) {
debug(' sending data browser file: ' + dataBrowserPath)
res.sendFile(dataBrowserPath)
return
} else if (stream) {
} else if (stream) { // EXIT text/html
res.setHeader('Content-Type', contentType)
return stream.pipe(res)
}
}

// If request accepts the content-type we found
// if (stream && negotiator.mediaType([contentType])) {
// res.setHeader('Content-Type', contentType)
// if (contentRange) {
// const headers = { 'Content-Range': contentRange, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize }
// res.writeHead(206, headers)
// return stream.pipe(res)
// } else {
// return stream.pipe(res)
// }
// }

if (stream && negotiator.mediaType([contentType])) {
res.setHeader('Content-Type', contentType)
let headers = {
'Content-Type': contentType
}
if (contentRange) {
const headers = { 'Content-Range': contentRange, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize }
res.writeHead(206, headers)
return stream.pipe(res)
} else {
return stream.pipe(res)
headers = {
...headers,
'Content-Range': contentRange,
'Accept-Ranges': 'bytes',
'Content-Length': chunksize
}
res.statusCode = 206
}

if (isRdf(contentType) && !res.sendEvents({
config: { prep: prepConfig },
body: stream,
isBodyStream: true,
headers
})) return
res.set(headers)
return stream.pipe(res)
}

// If it is not in our RDFs we can't even translate,
Expand All @@ -130,6 +157,14 @@ async function handler (req, res, next) {
// Translate from the contentType found to the possibleRDFType desired
const data = await translate(stream, baseUri, contentType, possibleRDFType)
debug(req.originalUrl + ' translating ' + contentType + ' -> ' + possibleRDFType)
const headers = {
'Content-Type': possibleRDFType
}
if (isRdf(contentType) && !res.sendEvents({
config: { prep: prepConfig },
body: data,
headers
})) return
res.setHeader('Content-Type', possibleRDFType)
res.send(data)
return next()
Expand Down
101 changes: 101 additions & 0 deletions lib/handlers/notify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
module.exports = handler

const libPath = require('path/posix')

const headerTemplate = require('express-prep/templates').header
const solidRDFTemplate = require('../rdf-notification-template')

const ALLOWED_RDF_MIME_TYPES = [
'application/ld+json',
'application/activity+json',
'text/turtle'
]

function getActivity (method) {
if (method === 'DELETE') {
return 'Delete'
}
return 'Update'
}

function getParentActivity (method, status) {
if (method === 'DELETE') {
return 'Remove'
}
if (status === 201) {
return 'Add'
}
return 'Update'
}

function handler (req, res, next) {
const { trigger, defaultNotification } = res.events.prep

const { method } = req
const { statusCode } = res
const eventID = res.getHeader('event-id')

const parent = `${libPath.dirname(req.path)}/`
const parentID = res.setEventID(parent)
const fullUrl = new URL(req.path, `${req.protocol}://${req.hostname}/`)
const parentUrl = new URL(parent, fullUrl)

// Date is a hack since node does not seem to provide access to send date.
// Date needs to be shared with parent notification
const eventDate = res._header.match(/^Date: (.*?)$/m)?.[1] ||
new Date().toUTCString()

// If the resource itself newly created,
// it could not have been subscribed for notifications already
if (!((method === 'PUT' || method === 'PATCH') && statusCode === 201)) {
trigger({
generateNotification (
negotiatedFields
) {
const mediaType = negotiatedFields['content-type']

if (ALLOWED_RDF_MIME_TYPES.includes(mediaType?.[0])) {
return `${headerTemplate(negotiatedFields)}\r\n${solidRDFTemplate({
activity: getActivity(method),
eventID,
object: String(fullUrl),
date: eventDate,
// We use eTag as a proxy for state for now
state: res.getHeader('ETag'),
mediaType
})}`
} else {
return defaultNotification({
...(res.method === 'POST') && { location: res.getHeader('Content-Location') }
})
}
}
})
}

// Write a notification to parent container
// POST in Solid creates a child resource
if (method !== 'POST') {
trigger({
path: parent,
generateNotification (
negotiatedFields
) {
const mediaType = negotiatedFields['content-type']
if (ALLOWED_RDF_MIME_TYPES.includes(mediaType?.[0])) {
return `${headerTemplate(negotiatedFields)}\r\n${solidRDFTemplate({
activity: getParentActivity(method, statusCode),
eventID: parentID,
date: eventDate,
object: String(parentUrl),
target: statusCode === 201 ? String(fullUrl) : undefined,
eTag: undefined,
mediaType
})}`
}
}
})
}

next()
}
2 changes: 2 additions & 0 deletions lib/handlers/patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ async function patchHandler (req, res, next) {
return writeGraph(graph, resource, ldp.resourceMapper.resolveFilePath(req.hostname), ldp.serverUri)
})

// Add event-id for notifications
res.setHeader('Event-ID', res.setEventID())
// Send the status and result to the client
res.status(resourceExists ? 200 : 201)
res.send(result)
Expand Down
4 changes: 4 additions & 0 deletions lib/handlers/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ async function handler (req, res, next) {
// Handled by backpressure of streams!
busboy.on('finish', function () {
debug('Done storing files')
// Add event-id for notifications
res.setHeader('Event-ID', res.setEventID())
res.sendStatus(200)
next()
})
Expand All @@ -91,6 +93,8 @@ async function handler (req, res, next) {
debug('File stored in ' + resourcePath)
header.addLinks(res, links)
res.set('Location', resourcePath)
// Add event-id for notifications
res.setHeader('Event-ID', res.setEventID())
res.sendStatus(201)
next()
},
Expand Down
2 changes: 2 additions & 0 deletions lib/handlers/put.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ async function putStream (req, res, next, stream = req) {
// Fails with Append on existing resource
if (!req.originalUrl.endsWith('.acl')) await checkPermission(req, resourceExists)
await ldp.put(req, stream, getContentType(req.headers))
// Add event-id for notifications
res.setHeader('Event-ID', res.setEventID())
res.sendStatus(resourceExists ? 204 : 201)
return next()
} catch (err) {
Expand Down
9 changes: 5 additions & 4 deletions lib/ldp-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const del = require('./handlers/delete')
const patch = require('./handlers/patch')
const index = require('./handlers/index')
const copy = require('./handlers/copy')
const notify = require('./handlers/notify')

function LdpMiddleware (corsSettings) {
const router = express.Router('/')
Expand All @@ -23,10 +24,10 @@ function LdpMiddleware (corsSettings) {

router.copy('/*', allow('Write'), copy)
router.get('/*', index, allow('Read'), header.addPermissions, get)
router.post('/*', allow('Append'), post)
router.patch('/*', allow('Append'), patch)
router.put('/*', allow('Append'), put)
router.delete('/*', allow('Write'), del)
router.post('/*', allow('Append'), post, notify)
router.patch('/*', allow('Append'), patch, notify)
router.put('/*', allow('Append'), put, notify)
router.delete('/*', allow('Write'), del, notify)

return router
}
66 changes: 66 additions & 0 deletions lib/rdf-notification-template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const CONTEXT_ACTIVITYSTREAMS = 'https://www.w3.org/ns/activitystreams'
const CONTEXT_NOTIFICATION = 'https://www.w3.org/ns/solid/notification/v1'
const CONTEXT_XML_SCHEMA = 'http://www.w3.org/2001/XMLSchema'

function generateJSONNotification ({
activity: type,
eventId: id,
date: published,
object,
target,
state = undefined
}) {
return {
published,
type,
id,
object,
...(type === 'Add') && { target },
...(type === 'Remove') && { origin: target },
...(state) && { state }
}
}

function generateTurtleNotification ({
activity,
eventId,
date,
object,
target,
state = undefined
}) {
const stateLine = `\n notify:state "${state}" ;`

return `@prefix as: <${CONTEXT_ACTIVITYSTREAMS}#> .
@prefix notify: <${CONTEXT_NOTIFICATION}#> .
@prefix xsd: <${CONTEXT_XML_SCHEMA}#> .

<${eventId}> a as:${activity} ;${state && stateLine}
as:object ${object} ;
as:published "${date}"^^xsd:dateTime .`.replaceAll('\n', '\r\n')
}

function serializeToJSONLD (notification, isActivityStreams = false) {
notification['@context'] = [CONTEXT_NOTIFICATION]
if (!isActivityStreams) {
notification['@context'].unshift(CONTEXT_ACTIVITYSTREAMS)
}
return JSON.stringify(notification, null, 2)
}

function rdfTemplate (props) {
const { mediaType } = props
if (mediaType[0] === 'application/activity+json' || (mediaType[0] === 'application/ld+json' && mediaType[1].get('profile')?.toLowerCase() === 'https://www.w3.org/ns/activitystreams')) {
return serializeToJSONLD(generateJSONNotification(props), true)
}

if (mediaType[0] === 'application/ld+json') {
return serializeToJSONLD(generateJSONNotification(props))
}

if (mediaType[0] === 'text/turtle') {
return generateTurtleNotification(props)
}
}

module.exports = rdfTemplate
Loading
Loading