mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Display symlinks in container listings.
Closes https://github.com/solid/community-server/issues/1015
This commit is contained in:
parent
9f241631f8
commit
2e4589938f
@ -142,7 +142,8 @@ export class FileDataAccessor implements DataAccessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the Stats object corresponding to the given file path.
|
* Gets the Stats object corresponding to the given file path,
|
||||||
|
* resolving symbolic links.
|
||||||
* @param path - File path to get info from.
|
* @param path - File path to get info from.
|
||||||
*
|
*
|
||||||
* @throws NotFoundHttpError
|
* @throws NotFoundHttpError
|
||||||
@ -150,7 +151,7 @@ export class FileDataAccessor implements DataAccessor {
|
|||||||
*/
|
*/
|
||||||
private async getStats(path: string): Promise<Stats> {
|
private async getStats(path: string): Promise<Stats> {
|
||||||
try {
|
try {
|
||||||
return await fsPromises.lstat(path);
|
return await fsPromises.stat(path);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (isSystemError(error) && error.code === 'ENOENT') {
|
if (isSystemError(error) && error.code === 'ENOENT') {
|
||||||
throw new NotFoundHttpError('', { cause: error });
|
throw new NotFoundHttpError('', { cause: error });
|
||||||
@ -273,16 +274,23 @@ export class FileDataAccessor implements DataAccessor {
|
|||||||
|
|
||||||
// For every child in the container we want to generate specific metadata
|
// For every child in the container we want to generate specific metadata
|
||||||
for await (const entry of dir) {
|
for await (const entry of dir) {
|
||||||
const childName = entry.name;
|
// Obtain details of the entry, resolving any symbolic links
|
||||||
|
const childPath = joinFilePath(link.filePath, entry.name);
|
||||||
|
let childStats;
|
||||||
|
try {
|
||||||
|
childStats = await this.getStats(childPath);
|
||||||
|
} catch {
|
||||||
|
// Skip this entry if details could not be retrieved (e.g., bad symbolic link)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Ignore non-file/directory entries in the folder
|
// Ignore non-file/directory entries in the folder
|
||||||
if (!entry.isFile() && !entry.isDirectory()) {
|
if (!childStats.isFile() && !childStats.isDirectory()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate the URI corresponding to the child resource
|
// Generate the URI corresponding to the child resource
|
||||||
const childLink = await this.resourceMapper
|
const childLink = await this.resourceMapper.mapFilePathToUrl(childPath, childStats.isDirectory());
|
||||||
.mapFilePathToUrl(joinFilePath(link.filePath, childName), entry.isDirectory());
|
|
||||||
|
|
||||||
// Hide metadata files
|
// Hide metadata files
|
||||||
if (childLink.isMetadata) {
|
if (childLink.isMetadata) {
|
||||||
@ -290,7 +298,6 @@ export class FileDataAccessor implements DataAccessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate metadata of this specific child
|
// Generate metadata of this specific child
|
||||||
const childStats = await fsPromises.lstat(joinFilePath(link.filePath, childName));
|
|
||||||
const metadata = new RepresentationMetadata(childLink.identifier);
|
const metadata = new RepresentationMetadata(childLink.identifier);
|
||||||
addResourceMetadata(metadata, childStats.isDirectory());
|
addResourceMetadata(metadata, childStats.isDirectory());
|
||||||
this.addPosixMetadata(metadata, childStats);
|
this.addPosixMetadata(metadata, childStats);
|
||||||
|
@ -117,7 +117,14 @@ describe('A FileDataAccessor', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('generates the metadata for a container.', async(): Promise<void> => {
|
it('generates the metadata for a container.', async(): Promise<void> => {
|
||||||
cache.data = { container: { resource: 'data', 'resource.meta': 'metadata', notAFile: 5, container2: {}}};
|
cache.data = {
|
||||||
|
container: {
|
||||||
|
resource: 'data',
|
||||||
|
'resource.meta': 'metadata',
|
||||||
|
notAFile: 5,
|
||||||
|
container2: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
metadata = await accessor.getMetadata({ path: `${base}container/` });
|
metadata = await accessor.getMetadata({ path: `${base}container/` });
|
||||||
expect(metadata.identifier.value).toBe(`${base}container/`);
|
expect(metadata.identifier.value).toBe(`${base}container/`);
|
||||||
expect(metadata.getAll(RDF.type)).toEqualRdfTermArray(
|
expect(metadata.getAll(RDF.type)).toEqualRdfTermArray(
|
||||||
@ -131,15 +138,50 @@ describe('A FileDataAccessor', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('generates metadata for container child resources.', async(): Promise<void> => {
|
it('generates metadata for container child resources.', async(): Promise<void> => {
|
||||||
cache.data = { container: { resource: 'data', 'resource.meta': 'metadata', notAFile: 5, container2: {}}};
|
cache.data = {
|
||||||
|
container: {
|
||||||
|
resource: 'data',
|
||||||
|
'resource.meta': 'metadata',
|
||||||
|
symlink: Symbol(`${rootFilePath}/container/resource`),
|
||||||
|
symlinkContainer: Symbol(`${rootFilePath}/container/container2`),
|
||||||
|
symlinkInvalid: Symbol(`${rootFilePath}/invalid`),
|
||||||
|
notAFile: 5,
|
||||||
|
container2: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const children = [];
|
const children = [];
|
||||||
for await (const child of accessor.getChildren({ path: `${base}container/` })) {
|
for await (const child of accessor.getChildren({ path: `${base}container/` })) {
|
||||||
children.push(child);
|
children.push(child);
|
||||||
}
|
}
|
||||||
expect(children).toHaveLength(2);
|
|
||||||
|
// Identifiers
|
||||||
|
expect(children).toHaveLength(4);
|
||||||
|
expect(new Set(children.map((child): string => child.identifier.value))).toEqual(new Set([
|
||||||
|
`${base}container/container2/`,
|
||||||
|
`${base}container/resource`,
|
||||||
|
`${base}container/symlink`,
|
||||||
|
`${base}container/symlinkContainer/`,
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Containers
|
||||||
|
for (const child of children.filter(({ identifier }): boolean => identifier.value.endsWith('/'))) {
|
||||||
|
const types = child.getAll(RDF.type).map((term): string => term.value);
|
||||||
|
expect(types).toContain(LDP.Resource);
|
||||||
|
expect(types).toContain(LDP.Container);
|
||||||
|
expect(types).toContain(LDP.BasicContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Documents
|
||||||
|
for (const child of children.filter(({ identifier }): boolean => !identifier.value.endsWith('/'))) {
|
||||||
|
const types = child.getAll(RDF.type).map((term): string => term.value);
|
||||||
|
expect(types).toContain(LDP.Resource);
|
||||||
|
expect(types).not.toContain(LDP.Container);
|
||||||
|
expect(types).not.toContain(LDP.BasicContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All resources
|
||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
expect([ `${base}container/resource`, `${base}container/container2/` ]).toContain(child.identifier.value);
|
|
||||||
expect(child.getAll(RDF.type)!.some((type): boolean => type.equals(LDP.terms.Resource))).toBe(true);
|
|
||||||
expect(child.get(DC.modified)).toEqualRdfTerm(toLiteral(now.toISOString(), XSD.terms.dateTime));
|
expect(child.get(DC.modified)).toEqualRdfTerm(toLiteral(now.toISOString(), XSD.terms.dateTime));
|
||||||
expect(child.get(POSIX.mtime)).toEqualRdfTerm(toLiteral(Math.floor(now.getTime() / 1000),
|
expect(child.get(POSIX.mtime)).toEqualRdfTerm(toLiteral(Math.floor(now.getTime() / 1000),
|
||||||
XSD.terms.integer));
|
XSD.terms.integer));
|
||||||
|
@ -109,7 +109,10 @@ export function mockFs(rootFilepath?: string, time?: Date): { data: any } {
|
|||||||
return stream;
|
return stream;
|
||||||
},
|
},
|
||||||
promises: {
|
promises: {
|
||||||
lstat(path: string): Stats {
|
async stat(path: string): Promise<Stats> {
|
||||||
|
return this.lstat(await this.realpath(path));
|
||||||
|
},
|
||||||
|
async lstat(path: string): Promise<Stats> {
|
||||||
const { folder, name } = getFolder(path);
|
const { folder, name } = getFolder(path);
|
||||||
if (!folder[name]) {
|
if (!folder[name]) {
|
||||||
throwSystemError('ENOENT');
|
throwSystemError('ENOENT');
|
||||||
@ -117,22 +120,32 @@ export function mockFs(rootFilepath?: string, time?: Date): { data: any } {
|
|||||||
return {
|
return {
|
||||||
isFile: (): boolean => typeof folder[name] === 'string',
|
isFile: (): boolean => typeof folder[name] === 'string',
|
||||||
isDirectory: (): boolean => typeof folder[name] === 'object',
|
isDirectory: (): boolean => typeof folder[name] === 'object',
|
||||||
|
isSymbolicLink: (): boolean => typeof folder[name] === 'symbol',
|
||||||
size: typeof folder[name] === 'string' ? folder[name].length : 0,
|
size: typeof folder[name] === 'string' ? folder[name].length : 0,
|
||||||
mtime: time,
|
mtime: time,
|
||||||
} as Stats;
|
} as Stats;
|
||||||
},
|
},
|
||||||
unlink(path: string): void {
|
async unlink(path: string): Promise<void> {
|
||||||
const { folder, name } = getFolder(path);
|
const { folder, name } = getFolder(path);
|
||||||
if (!folder[name]) {
|
if (!folder[name]) {
|
||||||
throwSystemError('ENOENT');
|
throwSystemError('ENOENT');
|
||||||
}
|
}
|
||||||
if (!this.lstat(path).isFile()) {
|
if (!(await this.lstat(path)).isFile()) {
|
||||||
throwSystemError('EISDIR');
|
throwSystemError('EISDIR');
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
delete folder[name];
|
delete folder[name];
|
||||||
},
|
},
|
||||||
rmdir(path: string): void {
|
async symlink(target: string, path: string): Promise<void> {
|
||||||
|
const { folder, name } = getFolder(path);
|
||||||
|
folder[name] = Symbol(target);
|
||||||
|
},
|
||||||
|
async realpath(path: string): Promise<string> {
|
||||||
|
const { folder, name } = getFolder(path);
|
||||||
|
const entry = folder[name];
|
||||||
|
return typeof entry === 'symbol' ? entry.description ?? 'invalid' : path;
|
||||||
|
},
|
||||||
|
async rmdir(path: string): Promise<void> {
|
||||||
const { folder, name } = getFolder(path);
|
const { folder, name } = getFolder(path);
|
||||||
if (!folder[name]) {
|
if (!folder[name]) {
|
||||||
throwSystemError('ENOENT');
|
throwSystemError('ENOENT');
|
||||||
@ -140,13 +153,13 @@ export function mockFs(rootFilepath?: string, time?: Date): { data: any } {
|
|||||||
if (Object.keys(folder[name]).length > 0) {
|
if (Object.keys(folder[name]).length > 0) {
|
||||||
throwSystemError('ENOTEMPTY');
|
throwSystemError('ENOTEMPTY');
|
||||||
}
|
}
|
||||||
if (!this.lstat(path).isDirectory()) {
|
if (!(await this.lstat(path)).isDirectory()) {
|
||||||
throwSystemError('ENOTDIR');
|
throwSystemError('ENOTDIR');
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
delete folder[name];
|
delete folder[name];
|
||||||
},
|
},
|
||||||
readdir(path: string): string[] {
|
async readdir(path: string): Promise<string[]> {
|
||||||
const { folder, name } = getFolder(path);
|
const { folder, name } = getFolder(path);
|
||||||
if (!folder[name]) {
|
if (!folder[name]) {
|
||||||
throwSystemError('ENOENT');
|
throwSystemError('ENOENT');
|
||||||
@ -158,29 +171,30 @@ export function mockFs(rootFilepath?: string, time?: Date): { data: any } {
|
|||||||
if (!folder[name]) {
|
if (!folder[name]) {
|
||||||
throwSystemError('ENOENT');
|
throwSystemError('ENOENT');
|
||||||
}
|
}
|
||||||
for (const child of Object.keys(folder[name])) {
|
for (const [ child, entry ] of Object.entries(folder[name])) {
|
||||||
yield {
|
yield {
|
||||||
name: child,
|
name: child,
|
||||||
isFile: (): boolean => typeof folder[name][child] === 'string',
|
isFile: (): boolean => typeof entry === 'string',
|
||||||
isDirectory: (): boolean => typeof folder[name][child] === 'object',
|
isDirectory: (): boolean => typeof entry === 'object',
|
||||||
|
isSymbolicLink: (): boolean => typeof entry === 'symbol',
|
||||||
} as Dirent;
|
} as Dirent;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mkdir(path: string): void {
|
async mkdir(path: string): Promise<void> {
|
||||||
const { folder, name } = getFolder(path);
|
const { folder, name } = getFolder(path);
|
||||||
if (folder[name]) {
|
if (folder[name]) {
|
||||||
throwSystemError('EEXIST');
|
throwSystemError('EEXIST');
|
||||||
}
|
}
|
||||||
folder[name] = {};
|
folder[name] = {};
|
||||||
},
|
},
|
||||||
readFile(path: string): string {
|
async readFile(path: string): Promise<string> {
|
||||||
const { folder, name } = getFolder(path);
|
const { folder, name } = getFolder(path);
|
||||||
if (!folder[name]) {
|
if (!folder[name]) {
|
||||||
throwSystemError('ENOENT');
|
throwSystemError('ENOENT');
|
||||||
}
|
}
|
||||||
return folder[name];
|
return folder[name];
|
||||||
},
|
},
|
||||||
writeFile(path: string, data: string): void {
|
async writeFile(path: string, data: string): Promise<void> {
|
||||||
const { folder, name } = getFolder(path);
|
const { folder, name } = getFolder(path);
|
||||||
folder[name] = data;
|
folder[name] = data;
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user