mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
Merge branch 'main' into versions/next-major
# Conflicts: # package-lock.json # package.json
This commit is contained in:
commit
4664c64e6c
4
.github/workflows/cth-test.yml
vendored
4
.github/workflows/cth-test.yml
vendored
@ -78,13 +78,13 @@ jobs:
|
||||
branch-name: ${{ inputs.branch || github.head_ref }}
|
||||
- name: Save the reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.sanitize.outputs.sanitized-branch-name }} reports
|
||||
path: reports
|
||||
- name: Save the server output
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.sanitize.outputs.sanitized-branch-name }} server output
|
||||
path: server-output.log
|
||||
|
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@ -89,6 +89,6 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm/v8,linux/arm64/v8
|
||||
platforms: linux/amd64
|
||||
tags: ${{ needs.docker-meta.outputs.tags }}
|
||||
labels: ${{ needs.docker-meta.outputs.labels }}
|
||||
|
2
.github/workflows/mkdocs.yml
vendored
2
.github/workflows/mkdocs.yml
vendored
@ -44,7 +44,7 @@ jobs:
|
||||
needs: mkdocs-prep
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: pip install mkdocs-material
|
||||
|
3
.github/workflows/npm-test.yml
vendored
3
.github/workflows/npm-test.yml
vendored
@ -27,6 +27,7 @@ jobs:
|
||||
- 18.x
|
||||
- '20.0'
|
||||
- 20.x
|
||||
- 21.x
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
@ -58,6 +59,7 @@ jobs:
|
||||
node-version:
|
||||
- 18.x
|
||||
- 20.x
|
||||
- 21.x
|
||||
env:
|
||||
TEST_DOCKER: true
|
||||
services:
|
||||
@ -92,6 +94,7 @@ jobs:
|
||||
node-version:
|
||||
- 18.x
|
||||
- 20.x
|
||||
- 21.x
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
debug-only: true
|
||||
stale-issue-label: 🏚️ abandoned
|
||||
|
31
CHANGELOG.md
31
CHANGELOG.md
@ -3,6 +3,37 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [7.0.2](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v7.0.1...v7.0.2) (2023-11-20)
|
||||
|
||||
### Features
|
||||
|
||||
* Add index signature to Credentials ([86f4592](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/86f45923ba6cc696615a98a5fbc8f13a525e4745))
|
||||
* Pass requestedModes metadata on 401 ([58daeb6](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/58daeb684f6e84fa0950d0ea5d9827881ba136c2))
|
||||
|
||||
### Fixes
|
||||
|
||||
* Add query parameter to static resources ([5f85441](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/5f85441b6e23a2bd2b3e1f3ef116a4868d5f9614))
|
||||
* Update resource size in ConstantConverter ([6c30a25](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/6c30a2512bd30bf52deb4526d13416016004646f))
|
||||
* Prevent errors in JSON storage when data is invalid ([4318479](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/43184791545bbf6fe8a840de07616fca8f3b7f97))
|
||||
* Prevent errors during migration for invalid JSON ([2f928bd](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/2f928bd2d4c8d4385bca4554ad0ed19cc5aaa770))
|
||||
* Disable submit buttons until form data is loaded ([1597a5a](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/1597a5a5782ca5b574c42ab3bab3d01de89ccf02))
|
||||
* Undo util.js errors introduced when changing lint settings ([261c3d0](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/261c3d05a6b446442f2ca5a4a0803363d1cb9021))
|
||||
|
||||
### Chores
|
||||
|
||||
* Update to TypeScript 5.2.2 ([edbf895](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/edbf895505d625d59404b82807616eebca757040))
|
||||
* Fix Dockerfile to Node v18 to prevent build issues ([9cc4a9c](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/9cc4a9ce4d786e76c54d126754271eb2ec1355a5))
|
||||
|
||||
### Documentation
|
||||
|
||||
* Explain storage/location import options ([01623e0](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/01623e0b5c58d77add4ab3e2a1a9082897ad5948))
|
||||
* Fix outdated information in IDP documentation ([#1773](https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1773)) ([15a929a](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/15a929a87e4ce00c0ed266e296405c8e4a22d4a7))
|
||||
* Explain the patching store in-depth ([4d05fe4](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/4d05fe4315e282d6e1ec19af01b185cb21cab29c))
|
||||
|
||||
### Refactors
|
||||
|
||||
* Replace linting configurations ([6248ed0](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/6248ed093813e95255f031eb8fcc37e4d869235c))
|
||||
|
||||
## [7.0.1](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v7.0.0...v7.0.1) (2023-10-20)
|
||||
|
||||
### Fixes
|
||||
|
@ -22,6 +22,17 @@ Used by certain classes for internal storage.
|
||||
* *memory*: Store everything in memory.
|
||||
* *resource-store*: Store everything in a specific container in the resource store.
|
||||
|
||||
## Location
|
||||
|
||||
Tells the server where it can find the storage root(s).
|
||||
Solid does not allow storage roots to be nested,
|
||||
so usually you either have one storage root at the server with no pods,
|
||||
or multiple storage roots at pod level with an inaccessible server root.
|
||||
|
||||
* *pod*: Indicates that the root storages are at pod level.
|
||||
If subdomains are used for pods, this will also include the actual server root.
|
||||
* *root*: There is only one storage root, and it is the same as the server root.
|
||||
|
||||
## Middleware
|
||||
|
||||
The chain of utility ResourceStores that needs to be passed through before reaching the backend stores.
|
||||
|
@ -29,6 +29,7 @@ the [changelog](https://github.com/CommunitySolidServer/CommunitySolidServer/blo
|
||||
|
||||
## Using the server
|
||||
|
||||
* [An overview of the main features of the server](features.md)
|
||||
* [Quickly starting the server](usage/starting-server.md)
|
||||
* [Basic example HTTP requests](usage/example-requests.md)
|
||||
* [Editing the metadata of a resource](usage/metadata.md)
|
||||
|
@ -76,7 +76,7 @@ The `urn:solid-server:default:AuthResourceHttpHandler` is identical
|
||||
to the `urn:solid-server:default:LdpHandler` which will be discussed below,
|
||||
but only handles resources relevant for authorization.
|
||||
|
||||
In practice this means that is your server is configured
|
||||
In practice this means that if your server is configured
|
||||
to use [Web Access Control](https://solidproject.org/TR/wac) for authorization,
|
||||
this handler will catch all requests targeting `.acl` resources.
|
||||
|
||||
|
92
documentation/markdown/features.md
Normal file
92
documentation/markdown/features.md
Normal file
@ -0,0 +1,92 @@
|
||||
# Features
|
||||
|
||||
Below is a non-exhaustive listing of the features available to a server instance,
|
||||
depending on the chosen configuration.
|
||||
The core feature of the CSS is that it uses **dependency injection** to configure its components,
|
||||
so any of the features below can always be adapted or replaced with custom components if required.
|
||||
It can also be used to configure dummy components for debugging, development, or experimentation purposes.
|
||||
See this [tutorial](https://github.com/CommunitySolidServer/tutorials/blob/main/custom-configurations.md)
|
||||
and/or this [example repository](https://github.com/CommunitySolidServer/hello-world-component)
|
||||
for more information on that.
|
||||
|
||||
To generate configurations with some of these features enabled/disabled,
|
||||
you can use the **[configuration generator](https://communitysolidserver.github.io/configuration-generator/)**.
|
||||
|
||||
## Authentication
|
||||
|
||||
Clients are identified based on the contents of **DPoP** tokens,
|
||||
as described in the [**Solid-OIDC** specification](https://solidproject.org/TR/oidc).
|
||||
|
||||
The server also provides several dummy components that can be used here,
|
||||
to either always identify the client as a fixed WebID,
|
||||
or to allow the WebID to be set directly in the `Authorization` header.
|
||||
These can be configured by changing the `ldp/authentication` import in your configuration.
|
||||
|
||||
## Authorization
|
||||
|
||||
Two authorization mechanisms are implemented for determining who has access to resources:
|
||||
|
||||
* **[Web Access Control](https://solidproject.org/TR/wac)**
|
||||
* **[Access Control Policy](https://solidproject.org/TR/acp)**
|
||||
|
||||
Alternatively, the server can be configured to not have any kind of authorization and allow full access to all resources.
|
||||
|
||||
## Solid Protocol
|
||||
|
||||
The **[Solid Protocol](https://solidproject.org/TR/protocol)** is supported.
|
||||
|
||||
Requests to the server support **content negotiation** for common RDF formats.
|
||||
|
||||
Binary **[range headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range)** are supported.
|
||||
|
||||
[`ETag`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) and `Last-Modified` headers are supported.
|
||||
These can be used for **[conditional requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests)**.
|
||||
|
||||
`PATCH` requests targeting RDF resources can be made with **N3 Patch** or **SPARQL Update** bodies.
|
||||
|
||||
The server can be configured to store data in **memory**, on the **file system**, or through a **SPARQL endpoint**.
|
||||
Similarly, the locking system that is used to prevent data conflicts
|
||||
can be configured to store locks in memory, on the file system, or in a Redis store, or it can be disabled.
|
||||
|
||||
Multiple **worker threads** can be used when starting the server.
|
||||
|
||||
## Account management
|
||||
|
||||
Accounts can be created on the server with which users can perform the following actions,
|
||||
through either a **JSON** or an **HTML** API:
|
||||
|
||||
* Add **email/password** combinations which can be used to log in.
|
||||
* Create **pods**, which are containers on the server over which the user has full control.
|
||||
* Link **WebIDs** to the account. When using [Solid-OIDC](https://solidproject.org/TR/oidc),
|
||||
the user can identify as any of these.
|
||||
For external WebIDs, the server requires the user to add a triple as identification,
|
||||
but this can be disabled if needed.
|
||||
* Create **client credentials**, which can be used to authenticate without using the browser.
|
||||
More information on these can be found [here](usage/client-credentials.md).
|
||||
* It is possible to use Solid-OIDC to limit access to certain parts of the account API.
|
||||
More information on this can be found [here](usage/identity-provider.md#access).
|
||||
|
||||
Using these accounts, the server can generate tokens
|
||||
to support **[Solid-OIDC](https://solidproject.org/TR/oidc)** authentication.
|
||||
|
||||
### Pods
|
||||
|
||||
The server keeps track of the **pod owners**,
|
||||
which is a list of WebIDs which have full control access over all resources contained within.
|
||||
Owners can be added to and removed from a pod.
|
||||
|
||||
Pod URLs can be minted as either
|
||||
**subdomain**, `http://pod.example.com/`, or **suffix**, `http://example.com/pod/`.
|
||||
|
||||
When starting the server, a configuration file can be provided
|
||||
to immediately create one or more accounts on the server with their own pods.
|
||||
See the [documentation](usage/seeding-pods.md) for more information.
|
||||
|
||||
## Notifications
|
||||
|
||||
CSS supports v0.2.0 of the **[Solid Notifications Protocol](https://solidproject.org/TR/notifications-protocol)**.
|
||||
Specifically it supports the Notification Types
|
||||
[WebSocketChannel2023](https://solid.github.io/notifications/websocket-channel-2023)
|
||||
and [WebhookChannel2023](https://solid.github.io/notifications/webhook-channel-2023).
|
||||
|
||||
More documentation on notifications can be found [here](usage/notifications.md).
|
@ -26,6 +26,13 @@ Below is an example of how to call the API to generate such a token.
|
||||
The code below generates a token linked to your account and WebID.
|
||||
This only needs to be done once, afterwards this token can be used for all future requests.
|
||||
|
||||
Before doing the step below,
|
||||
you already need to have an [authorization value](account/json-api.md#authorization)
|
||||
that you get after logging in to your account.
|
||||
In the example below the cookie value is used.
|
||||
In the default server configurations,
|
||||
you can log in through the [email/password API](account/json-api.md#controlspasswordlogin).
|
||||
|
||||
```ts
|
||||
// This assumes your server is started under http://localhost:3000/.
|
||||
// It also assumes you have already logged in and `cookie` contains a valid cookie header
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Besides implementing the [Solid protocol](https://solidproject.org/TR/protocol),
|
||||
the community server can also be an Identity Provider (IDP), officially known as an OpenID Provider (OP),
|
||||
following the [Solid OIDC spec](https://solid.github.io/solid-oidc/) as much as possible.
|
||||
following the [Solid-OIDC specification](https://solid.github.io/solid-oidc/) as much as possible.
|
||||
|
||||
It is recommended to use the latest version
|
||||
of the [Solid authentication client](https://github.com/inrupt/solid-client-authn-js)
|
||||
@ -91,11 +91,11 @@ Below we go a bit deeper into the available options
|
||||
The `access` option allows you to set authorization restrictions on the IDP API when enabled,
|
||||
similar to how authorization works on the LDP requests on the server.
|
||||
For example, if the server uses WebACL as authorization scheme,
|
||||
you can put a `.acl` resource in the `/idp/register/` container to restrict
|
||||
who is allowed to access the registration API.
|
||||
Note that for everything to work there needs to be a `.acl` resource in `/idp/` when using WebACL
|
||||
you can put a `.acl` resource in the `/.account/account/` container to restrict
|
||||
who is allowed to access the account creation API.
|
||||
Note that for everything to work there needs to be a `.acl` resource in `/.account/` when using WebACL
|
||||
so resources can be accessed as usual when the server starts up.
|
||||
Make sure you change the permissions on `/idp/.acl` so not everyone can modify those.
|
||||
Make sure you change the permissions on `/.account/.acl` so not everyone can modify those.
|
||||
|
||||
All of the above is only relevant if you use the `restricted.json` setting for this import.
|
||||
When you use `public.json` the API will simply always be accessible by everyone.
|
||||
@ -104,20 +104,33 @@ When you use `public.json` the API will simply always be accessible by everyone.
|
||||
|
||||
In case you want users to be able to reset their password when they forget it,
|
||||
you will need to tell the server which email server to use to send reset mails.
|
||||
`example.json` contains an example of what this looks like,
|
||||
which you will need to copy over to your base configuration and then remove the `config/identity/email` import.
|
||||
`example.json` contains an example of what this looks like.
|
||||
When using this import, you can override the values with those of your own mail client
|
||||
by adding the following to your `Components.js` configuration with updated values:
|
||||
|
||||
```json
|
||||
{
|
||||
"comment": "The settings of your email server.",
|
||||
"@type": "Override",
|
||||
"overrideInstance": {
|
||||
"@id": "urn:solid-server:default:EmailSender"
|
||||
},
|
||||
"overrideParameters": {
|
||||
"@type": "BaseEmailSender",
|
||||
"senderName": "Community Solid Server <solid@example.email>",
|
||||
"emailConfig_host": "smtp.example.email",
|
||||
"emailConfig_port": 587,
|
||||
"emailConfig_auth_user": "solid@example.email",
|
||||
"emailConfig_auth_pass": "NYEaCsqV7aVStRCbmC"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### handler
|
||||
|
||||
There is only one option here. This import contains all the core components necessary to make the IDP work.
|
||||
In case you need to make some changes to core IDP settings, this is where you would have to look.
|
||||
|
||||
### interaction
|
||||
|
||||
Here you determine which features of account management are available.
|
||||
`default.json` allows everything, while `no-accounts.json` and `no-pods.json`
|
||||
disable account and pod creation respectively.
|
||||
Taking one of those latter options will disable the relevant JSON APIs and HTML pages.
|
||||
`default.json` allows everything, `disabled.json` completely disables account management,
|
||||
and the other options disable account and/or pod creation.
|
||||
|
||||
### pod
|
||||
|
||||
|
@ -77,6 +77,8 @@ extra:
|
||||
nav:
|
||||
- Welcome:
|
||||
- README.md
|
||||
- Features:
|
||||
- features.md
|
||||
- Usage:
|
||||
- Example request: usage/example-requests.md
|
||||
- Metadata: usage/metadata.md
|
||||
|
@ -1,10 +1,18 @@
|
||||
const antfu = require('@antfu/eslint-config').default;
|
||||
const antfu = require('@antfu/eslint-config');
|
||||
const generalConfig = require('./eslint/general');
|
||||
const testConfig = require('./eslint/test');
|
||||
const typedConfig = require('./eslint/typed');
|
||||
const unicornConfig = require('./eslint/unicorn');
|
||||
|
||||
const configs = antfu(
|
||||
// The default ignore list contains all `output` folders, which conflicts with our src/http/output folder
|
||||
// See https://github.com/antfu/eslint-config/blob/7071af7024335aad319a91db41ce594ebc6a0899/src/globs.ts#L55
|
||||
const index = antfu.GLOB_EXCLUDE.indexOf('**/output');
|
||||
if (index < 0) {
|
||||
throw new Error('Could not update GLOB_EXCLUDE. Check if antfu changed how it handles ignores.');
|
||||
}
|
||||
antfu.GLOB_EXCLUDE.splice(index, 1);
|
||||
|
||||
module.exports = antfu.default(
|
||||
{
|
||||
// Don't want to lint test assets, or TS snippets in markdown files
|
||||
ignores: [ 'test/assets/*', '**/*.md/**/*.ts' ],
|
||||
@ -35,12 +43,3 @@ const configs = antfu(
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// The default ignore list contains all `output` folders, which conflicts with our src/http/output folder
|
||||
// See https://github.com/antfu/eslint-config/blob/29f29f1e16d0187f5c870102f910d798acd9b874/src/globs.ts#L53
|
||||
if (!configs[1].ignores.includes('**/output')) {
|
||||
throw new Error('Unexpected data in config position. Check if antfu changed how it handles ignores.');
|
||||
}
|
||||
delete configs[1].ignores;
|
||||
|
||||
module.exports = configs;
|
||||
|
@ -61,6 +61,8 @@ module.exports = {
|
||||
'style/block-spacing': 'off',
|
||||
'style/brace-style': [ 'error', '1tbs', { allowSingleLine: false }],
|
||||
'style/generator-star-spacing': [ 'error', { before: false, after: true }],
|
||||
// Seems to be inconsistent in when it adds indentation and when it does not
|
||||
'style/indent-binary-ops': 'off',
|
||||
'style/member-delimiter-style': [ 'error', {
|
||||
multiline: { delimiter: 'semi', requireLast: true },
|
||||
singleline: { delimiter: 'semi', requireLast: false },
|
||||
|
5534
package-lock.json
generated
5534
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@solid/community-server",
|
||||
"version": "7.0.1",
|
||||
"version": "7.0.2",
|
||||
"description": "Community Solid Server: an open and modular implementation of the Solid specifications",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/CommunitySolidServer/CommunitySolidServer#readme",
|
||||
@ -143,7 +143,7 @@
|
||||
"yup": "^1.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^1.0.0-beta.27",
|
||||
"@antfu/eslint-config": "2.3.4",
|
||||
"@commitlint/cli": "^18.2.0",
|
||||
"@commitlint/config-conventional": "^18.0.0",
|
||||
"@inrupt/solid-client-authn-core": "^1.17.3",
|
||||
|
@ -36,7 +36,8 @@ export class StorageDescriptionAdvertiser extends MetadataWriter {
|
||||
storageRoot = await this.storageStrategy.getStorageIdentifier(identifier);
|
||||
this.logger.debug(`Found storage root ${storageRoot.path}`);
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Unable to find storage root: ${createErrorMessage(error)}`);
|
||||
this.logger.error(`Unable to find storage root: ${createErrorMessage(error)
|
||||
}. The storage/location import in the server configuration is probably wrong.`);
|
||||
return;
|
||||
}
|
||||
const storageDescription = joinUrl(storageRoot.path, this.relativePath);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||
import { JsonResourceStorage } from '../../storage/keyvalue/JsonResourceStorage';
|
||||
import { createErrorMessage } from '../../util/errors/ErrorUtil';
|
||||
import { isContainerIdentifier } from '../../util/PathUtil';
|
||||
import { readableToString } from '../../util/StreamUtil';
|
||||
import { LDP } from '../../util/Vocabularies';
|
||||
@ -34,9 +35,14 @@ export class SingleContainerJsonStorage<T> extends JsonResourceStorage<T> {
|
||||
continue;
|
||||
}
|
||||
|
||||
const json = JSON.parse(await readableToString(document.data)) as T;
|
||||
const key = this.identifierToKey(documentId);
|
||||
try {
|
||||
const json = JSON.parse(await readableToString(document.data)) as T;
|
||||
yield [ key, json ];
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Unable to parse ${path}. You should probably delete this resource manually. Error: ${
|
||||
createErrorMessage(error)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,15 @@
|
||||
import type { Stats } from 'node:fs';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { stat } from 'fs-extra';
|
||||
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
|
||||
import type { Representation } from '../../http/representation/Representation';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import { createErrorMessage } from '../../util/errors/ErrorUtil';
|
||||
import { InternalServerError } from '../../util/errors/InternalServerError';
|
||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||
import { isContainerIdentifier } from '../../util/PathUtil';
|
||||
import { toLiteral } from '../../util/TermUtil';
|
||||
import { POSIX, XSD } from '../../util/Vocabularies';
|
||||
import { cleanPreferences, getTypeWeight, matchesMediaType } from './ConversionUtil';
|
||||
import { RepresentationConverter } from './RepresentationConverter';
|
||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||
@ -46,6 +53,8 @@ export interface ConstantConverterOptions {
|
||||
* Options default to the most permissive values when not defined.
|
||||
*/
|
||||
export class ConstantConverter extends RepresentationConverter {
|
||||
private readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly filePath: string;
|
||||
private readonly contentType: string;
|
||||
private readonly options: Required<ConstantConverterOptions>;
|
||||
@ -113,8 +122,19 @@ export class ConstantConverter extends RepresentationConverter {
|
||||
// Ignore the original representation
|
||||
representation.data.destroy();
|
||||
|
||||
// Get the stats to have the correct size metadata
|
||||
let stats: Stats;
|
||||
try {
|
||||
stats = await stat(this.filePath);
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Unable to access ${this.filePath}: ${createErrorMessage(error)}`);
|
||||
// Not giving out details in error as it contains internal server information
|
||||
throw new InternalServerError(`Unable to access file used for constant conversion.`);
|
||||
}
|
||||
|
||||
// Create a new representation from the constant file
|
||||
const data = createReadStream(this.filePath, 'utf8');
|
||||
representation.metadata.set(POSIX.terms.size, toLiteral(stats.size, XSD.terms.integer));
|
||||
return new BasicRepresentation(data, representation.metadata, this.contentType);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { BasicRepresentation } from '../../http/representation/BasicRepresentati
|
||||
import type { Representation } from '../../http/representation/Representation';
|
||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import { createErrorMessage } from '../../util/errors/ErrorUtil';
|
||||
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
||||
import { ensureTrailingSlash, isContainerIdentifier, joinUrl, trimLeadingSlashes } from '../../util/PathUtil';
|
||||
import { readableToString } from '../../util/StreamUtil';
|
||||
@ -88,8 +89,14 @@ export class JsonResourceStorage<T> implements KeyValueStorage<string, T> {
|
||||
yield* this.getResourceEntries({ path });
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const json = JSON.parse(await readableToString(representation.data)) as T;
|
||||
yield [ this.identifierToKey(identifier), json ];
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Unable to parse ${identifier.path
|
||||
}. You should probably delete this resource manually. Error: ${
|
||||
createErrorMessage(error)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -144,7 +144,6 @@ export function transformSafely<T = any>(
|
||||
source: NodeJS.ReadableStream,
|
||||
{
|
||||
transform = function(data): void {
|
||||
// eslint-disable-next-line ts/no-invalid-this
|
||||
this.push(data);
|
||||
},
|
||||
flush = (): null => null,
|
||||
|
@ -4,8 +4,8 @@
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title><%= extractTitle(htmlBody) %></title>
|
||||
<link rel="stylesheet" href="<%= baseUrl -%>.well-known/css/styles/main.css" type="text/css">
|
||||
<script type="text/javascript" src="<%= baseUrl -%>.well-known/css/scripts/util.js"></script>
|
||||
<link rel="stylesheet" href="<%= baseUrl -%>.well-known/css/styles/main.css?v=7" type="text/css">
|
||||
<script type="text/javascript" src="<%= baseUrl -%>.well-known/css/scripts/util.js?v=7"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
|
@ -0,0 +1 @@
|
||||
this is not valid JSON
|
@ -63,6 +63,8 @@ describe('A server migrating from v6', (): void => {
|
||||
// Setup resources should have been migrated
|
||||
const setupDir = await readdir(joinFilePath(rootFilePath, '.internal/setup/'));
|
||||
expect(setupDir).toEqual([
|
||||
// Invalid JSON file was not deleted, only error was logged. Just in case its data needs to be saved.
|
||||
'aW52YWxpZFJlc291cmNl$.json',
|
||||
'current-base-url$.json',
|
||||
'current-server-version$.json',
|
||||
'setupCompleted-2.0$.json',
|
||||
|
@ -310,6 +310,7 @@ describe('An IdentityProviderFactory', (): void => {
|
||||
expect(use).toHaveBeenCalledTimes(1);
|
||||
const middleware = use.mock.calls[0][0];
|
||||
|
||||
// eslint-disable-next-line jest/unbound-method
|
||||
const oldAccept = ctx.accepts;
|
||||
const next = jest.fn();
|
||||
await expect(middleware(ctx, next)).resolves.toBeUndefined();
|
||||
@ -325,6 +326,7 @@ describe('An IdentityProviderFactory', (): void => {
|
||||
expect(use).toHaveBeenCalledTimes(1);
|
||||
const middleware = use.mock.calls[0][0];
|
||||
|
||||
// eslint-disable-next-line jest/unbound-method
|
||||
const oldAccept = ctx.accepts;
|
||||
const next = jest.fn();
|
||||
await expect(middleware(ctx, next)).resolves.toBeUndefined();
|
||||
|
@ -22,8 +22,8 @@ describe('A ClusterManager', (): void => {
|
||||
beforeAll((): void => {
|
||||
Object.assign(mockCluster, {
|
||||
fork: jest.fn().mockImplementation((): any => mockWorker),
|
||||
on: jest.fn().mockImplementation(emitter.on),
|
||||
emit: jest.fn().mockImplementation(emitter.emit),
|
||||
on: jest.fn().mockImplementation(emitter.on.bind(emitter)),
|
||||
emit: jest.fn().mockImplementation(emitter.emit.bind(emitter)),
|
||||
isMaster: true,
|
||||
isWorker: false,
|
||||
});
|
||||
|
@ -19,6 +19,7 @@ describe('A SingleContainerJsonStorage', (): void => {
|
||||
if (isContainerIdentifier(id)) {
|
||||
const metadata = new RepresentationMetadata(id);
|
||||
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/foo');
|
||||
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/bad');
|
||||
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/bar/');
|
||||
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/baz');
|
||||
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/unknown');
|
||||
@ -27,14 +28,17 @@ describe('A SingleContainerJsonStorage', (): void => {
|
||||
if (id.path.endsWith('unknown')) {
|
||||
throw new NotFoundHttpError();
|
||||
}
|
||||
return new BasicRepresentation(`{ "id": "${id.path}" }`, 'text/plain');
|
||||
if (id.path.endsWith('bad')) {
|
||||
return new BasicRepresentation(`invalid JSON`, 'application/json');
|
||||
}
|
||||
return new BasicRepresentation(`{ "id": "${id.path}" }`, 'application/json');
|
||||
}),
|
||||
} satisfies Partial<ResourceStore> as any;
|
||||
|
||||
storage = new SingleContainerJsonStorage(store, baseUrl, container);
|
||||
});
|
||||
|
||||
it('only iterates over the documents in the base container.', async(): Promise<void> => {
|
||||
it('only iterates over the valid documents in the base container.', async(): Promise<void> => {
|
||||
const entries = [];
|
||||
for await (const entry of storage.entries()) {
|
||||
entries.push(entry);
|
||||
@ -43,7 +47,7 @@ describe('A SingleContainerJsonStorage', (): void => {
|
||||
[ 'foo', { id: 'http://example.com/.internal/accounts/foo' }],
|
||||
[ 'baz', { id: 'http://example.com/.internal/accounts/baz' }],
|
||||
]);
|
||||
expect(store.getRepresentation).toHaveBeenCalledTimes(4);
|
||||
expect(store.getRepresentation).toHaveBeenCalledTimes(5);
|
||||
expect(store.getRepresentation).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{ path: 'http://example.com/.internal/accounts/' },
|
||||
@ -56,11 +60,16 @@ describe('A SingleContainerJsonStorage', (): void => {
|
||||
);
|
||||
expect(store.getRepresentation).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
{ path: 'http://example.com/.internal/accounts/baz' },
|
||||
{ path: 'http://example.com/.internal/accounts/bad' },
|
||||
{ type: { 'application/json': 1 }},
|
||||
);
|
||||
expect(store.getRepresentation).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
{ path: 'http://example.com/.internal/accounts/baz' },
|
||||
{ type: { 'application/json': 1 }},
|
||||
);
|
||||
expect(store.getRepresentation).toHaveBeenNthCalledWith(
|
||||
5,
|
||||
{ path: 'http://example.com/.internal/accounts/unknown' },
|
||||
{ type: { 'application/json': 1 }},
|
||||
);
|
||||
|
@ -819,7 +819,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
const auxResourceID = { path: `${root}resource.dummy` };
|
||||
accessor.data[resourceID.path] = representation;
|
||||
accessor.data[auxResourceID.path] = representation;
|
||||
const deleteFn = accessor.deleteResource;
|
||||
const deleteFn = accessor.deleteResource.bind(accessor);
|
||||
jest.spyOn(accessor, 'deleteResource')
|
||||
.mockImplementation(async(identifier: ResourceIdentifier): Promise<void> => {
|
||||
if (auxiliaryStrategy.isAuxiliaryIdentifier(identifier)) {
|
||||
@ -862,6 +862,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
||||
|
||||
it('should rethrow any unexpected errors from validateIdentifier.', async(): Promise<void> => {
|
||||
const resourceID = { path: `${root}resource` };
|
||||
// eslint-disable-next-line jest/unbound-method
|
||||
const originalMetaData = accessor.getMetadata;
|
||||
jest.spyOn(accessor, 'getMetadata').mockImplementation(async(): Promise<any> => {
|
||||
throw new Error('error');
|
||||
|
@ -33,7 +33,7 @@ describe('A LockingResourceStore', (): void => {
|
||||
}
|
||||
|
||||
const readable = guardedStreamFrom([ 1, 2, 3 ]);
|
||||
const { destroy } = readable;
|
||||
const destroy = readable.destroy.bind(readable);
|
||||
jest.spyOn(readable, 'destroy').mockImplementation((error): any => destroy.call(readable, error));
|
||||
source = {
|
||||
getRepresentation: jest.fn((): any => addOrder('getRepresentation', { data: readable } as Representation)),
|
||||
|
@ -1,11 +1,13 @@
|
||||
import fs from 'node:fs';
|
||||
import fsExtra from 'fs-extra';
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
||||
import type { ConstantConverterOptions } from '../../../../src/storage/conversion/ConstantConverter';
|
||||
import { ConstantConverter } from '../../../../src/storage/conversion/ConstantConverter';
|
||||
import { CONTENT_TYPE } from '../../../../src/util/Vocabularies';
|
||||
import { CONTENT_TYPE, POSIX } from '../../../../src/util/Vocabularies';
|
||||
|
||||
const createReadStream = jest.spyOn(fs, 'createReadStream').mockReturnValue('file contents' as any);
|
||||
const stat = jest.spyOn(fsExtra, 'stat').mockReturnValue({ size: 100 } as any);
|
||||
|
||||
describe('A ConstantConverter', (): void => {
|
||||
const identifier = { path: 'identifier' };
|
||||
@ -13,6 +15,7 @@ describe('A ConstantConverter', (): void => {
|
||||
let converter: ConstantConverter;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
jest.clearAllMocks();
|
||||
options = { container: true, document: true, minQuality: 1, enabledMediaRanges: [ '*/*' ], disabledMediaRanges: []};
|
||||
converter = new ConstantConverter('abc/def/index.html', 'text/html', options);
|
||||
});
|
||||
@ -99,7 +102,7 @@ describe('A ConstantConverter', (): void => {
|
||||
await expect(converter.canHandle(args)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('replaces the representation of a supported request.', async(): Promise<void> => {
|
||||
it('replaces the representation of a supported request and replaces the size.', async(): Promise<void> => {
|
||||
const preferences = { type: { 'text/html': 1 }};
|
||||
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
|
||||
const representation = { metadata, data: { destroy: jest.fn() }} as any;
|
||||
@ -114,9 +117,28 @@ describe('A ConstantConverter', (): void => {
|
||||
expect(createReadStream).toHaveBeenCalledWith('abc/def/index.html', 'utf8');
|
||||
|
||||
expect(converted.metadata.contentType).toBe('text/html');
|
||||
expect(converted.metadata.get(POSIX.terms.size)?.value).toBe('100');
|
||||
await expect(arrayifyStream(converted.data)).resolves.toEqual([ 'file contents' ]);
|
||||
});
|
||||
|
||||
it('throws an internal error if the file cannot be accessed.', async(): Promise<void> => {
|
||||
const preferences = { type: { 'text/html': 1 }};
|
||||
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
|
||||
const representation = { metadata, data: { destroy: jest.fn() }} as any;
|
||||
const args = { identifier, representation, preferences };
|
||||
|
||||
await expect(converter.canHandle(args)).resolves.toBeUndefined();
|
||||
|
||||
// eslint-disable-next-line ts/no-misused-promises
|
||||
stat.mockImplementation(async(): Promise<never> => {
|
||||
throw new Error('file not found');
|
||||
});
|
||||
|
||||
await expect(converter.handle(args)).rejects.toThrow('Unable to access file used for constant conversion.');
|
||||
expect(representation.data.destroy).toHaveBeenCalledTimes(1);
|
||||
expect(createReadStream).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('defaults to the most permissive options.', async(): Promise<void> => {
|
||||
const preferences = { type: { 'text/html': 0.1 }};
|
||||
const metadata = new RepresentationMetadata();
|
||||
|
@ -5,7 +5,7 @@ import type { ResourceIdentifier } from '../../../../src/http/representation/Res
|
||||
import { JsonResourceStorage } from '../../../../src/storage/keyvalue/JsonResourceStorage';
|
||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
||||
import { isContainerIdentifier } from '../../../../src/util/PathUtil';
|
||||
import { isContainerIdentifier, joinUrl } from '../../../../src/util/PathUtil';
|
||||
import { readableToString } from '../../../../src/util/StreamUtil';
|
||||
import { LDP } from '../../../../src/util/Vocabularies';
|
||||
|
||||
@ -112,6 +112,9 @@ describe('A JsonResourceStorage', (): void => {
|
||||
data.set(containerIdentifier, '');
|
||||
data.set(subContainerIdentifier, '');
|
||||
|
||||
// Manually setting invalid data which will be ignored
|
||||
data.set(joinUrl(containerIdentifier, 'badData'), 'invalid JSON');
|
||||
|
||||
const entries = [];
|
||||
for await (const entry of storage.entries()) {
|
||||
entries.push(entry);
|
||||
|
Loading…
x
Reference in New Issue
Block a user