ownfiles
Version:
A library to manage files in a Solid User's Pod
358 lines (343 loc) • 12.9 kB
text/typescript
import * as urlUtils from 'url';
import cuid from 'cuid';
import FileClient from './index';
import * as rdf from 'rdflib';
import { IndexEntry } from './deepRead';
const ns = require('solid-namespace')(rdf);
export type IndexType = IndexEntry[];
export const createIndex = async function(
this: FileClient,
url: string,
options?: Partial<{ items: IndexType; foundCallback: (item) => unknown }>,
) {
const parsedUrl = urlUtils.parse(url);
const rootUrl = `${parsedUrl.protocol}//${parsedUrl.host}/`;
const indexUrl = rootUrl + this.indexPath;
await this.createIfNotExist(indexUrl);
this.indexURI = indexUrl;
const items =
(options?.items as IndexEntry[]) ??
((await this.deepRead(url, {
verbose: true,
foundCallback: options?.foundCallback,
})) as IndexEntry[]);
const { ins } = getNewIndexTriples(items, this.graph, indexUrl);
await this.updater.put(
rdf.sym(indexUrl),
ins,
'text/turtle',
(_, ok, error) => {
if (!ok) throw error;
},
);
return this.readIndex(indexUrl);
};
export const deleteIndex = async function(this: FileClient, url: string) {
const parsedUrl = urlUtils.parse(url);
const rootUrl = `${parsedUrl.protocol}//${parsedUrl.host}/`;
const indexUrl = rootUrl + this.indexPath;
return this.delete(indexUrl)
.then(() => (this.indexURI = undefined))
.catch((err) => {
if (err.response.status === 404) {
return;
} else {
throw err;
}
});
};
export const readIndex = function(this: FileClient, url: string) {
const parsedUrl = urlUtils.parse(url);
const rootUrl = `${parsedUrl.protocol}//${parsedUrl.host}/`;
const indexUrl = rootUrl + this.indexPath;
return this.fetcher.load(indexUrl).then(() => {
const index = readIndexTriples(indexUrl, this.graph);
return index;
});
};
const readIndexTriples = (indexUrl: string, graph: any) => {
const index: IndexEntry[] = [];
graph
.statementsMatching(
null,
ns.rdf('type'),
ns.solid('TypeRegistration'),
rdf.sym(indexUrl),
)
.forEach(({ subject }: { subject: { value: string } }) => {
const instanceStatement = ((graph.any(
subject,
ns.solid('instance'),
) &&
graph.statementsMatching(subject, ns.solid('instance'))) ??
(graph.any(subject, ns.solid('instanceContainer')) &&
graph.statementsMatching(
subject,
ns.solid('instanceContainer'),
)))[0];
const typeNodes = graph
.statementsMatching(subject, ns.solid('forClass'))
.map(
(statement: { object: { value: string } }) =>
statement.object.value,
) as string[];
index.push(
instanceStatement.predicate.value === ns.solid('instance')
? {
url: instanceStatement.object.value,
types: typeNodes,
}
: {
url: instanceStatement.object.value,
types: typeNodes,
},
);
});
return index;
};
export const deleteFromIndex = async function(this: FileClient, item: string) {
const parsedUrl = urlUtils.parse(item);
const rootUrl = `${parsedUrl.protocol}//${parsedUrl.host}/`;
const indexUrl = rootUrl + this.indexPath;
const itemsToDelete = await this.deepRead(item);
await this.fetcher.load(indexUrl);
if (item === indexUrl) return Promise.resolve([]);
return await Promise.all(
itemsToDelete.map((item) => {
item = item as string;
const rootNode = (this.graph.any(
null,
ns.solid('instance'),
rdf.sym(item),
) ||
this.graph.any(
null,
ns.solid('instanceContainer'),
rdf.sym(item),
)) as rdf.Variable;
return this.updater.update(
this.graph.statementsMatching(rootNode),
[],
(_, ok, err) => {
if (!ok) {
console.log(err);
}
},
);
}),
);
};
export const addToIndex = async function(
this: FileClient,
item: IndexEntry | string,
{
force,
updateCallback,
}: {
force?: boolean;
updateCallback?: (
uri: string | undefined | null,
success: boolean,
errorBody?: string,
response?: Response | Error,
) => void;
} = {},
) {
const parsedItemUrl =
typeof item === 'string'
? urlUtils.parse(item)
: urlUtils.parse(item.url);
const rootUrl = `${parsedItemUrl.protocol}//${parsedItemUrl.host}/`;
const indexUrl = rootUrl + this.indexPath;
const itemsToAdd =
!force && typeof item === 'string'
? ((await this.deepRead(item, {
verbose: true,
})) as IndexEntry[])
: ([item] as IndexEntry[]);
if (!this.indexURI) {
const res = await this.createIfNotExist(indexUrl);
this.indexURI = res?.headers.get('Location') ?? indexUrl;
}
const index = !force ? ((await this.readIndex(rootUrl)) as IndexType) : [];
const itemsToUpdate = index.reduce((itemsToUpdate: IndexEntry[], entry) => {
const entryUrl = urlUtils.parse(entry.url);
return parsedItemUrl.pathname?.includes(entryUrl.pathname as string)
? [...itemsToUpdate, entry]
: itemsToUpdate;
}, []);
const { del, ins } = getNewIndexTriples(
[...(itemsToUpdate ?? []), ...itemsToAdd],
this.graph,
indexUrl,
);
return await this.updater.update(
del,
ins,
updateCallback ??
((_, ok, err) => {
if (!ok) {
console.log(err);
}
}),
);
};
const getNewIndexTriples = (
items: IndexEntry[],
graph: any,
indexUrl: string,
) => {
const del: rdf.Statement[] = [];
const ins: rdf.Statement[] = [];
items
.filter((item) => item.url !== indexUrl)
.forEach((item) => {
if (item && item.types.includes(ns.ldp('Container').value)) {
let rootNode = graph.any(
null,
ns.solid('instanceContainer'),
rdf.sym(item.url),
);
if (!rootNode) {
rootNode = new rdf.NamedNode(`${indexUrl}#${cuid()}`);
ins.push(
rdf.st(
rootNode,
ns.rdf('type'),
ns.solid('TypeRegistration'),
rdf.sym(indexUrl),
),
);
ins.push(
rdf.st(
rootNode,
ns.solid('instanceContainer'),
rdf.sym(item.url),
rdf.sym(indexUrl),
),
);
if (item.types.length === 0) {
graph
.statementsMatching(rootNode, ns.solid('forClass'))
.forEach((st) => del.push(st));
} else {
item.types.forEach((type) => {
ins.push(
rdf.st(
rootNode,
ns.solid('forClass'),
rdf.sym(type),
rdf.sym(indexUrl),
),
);
});
}
} else {
const typesFound = graph
.each(rootNode, ns.solid('forClass'), null)
.map((node: { value: string }) => node.value);
const typesToDelete = typesFound.reduce(
(notFoundTypes: string[], type: string) =>
!item.types.includes(type)
? Array.isArray(notFoundTypes)
? [...notFoundTypes, type]
: [type]
: notFoundTypes,
[],
);
const typesToAdd = item.types.reduce(
(notFoundTypes: string[], type: string) =>
!typesFound.includes(type)
? [...notFoundTypes, type]
: notFoundTypes,
[],
);
typesToDelete.forEach((type: string) =>
graph
.statementsMatching(
rootNode,
ns.solid('forClass'),
rdf.sym(type),
)
.forEach((st: rdf.Statement) => del.push(st)),
);
typesToAdd.forEach((type: string) =>
ins.push(
rdf.st(
rootNode,
ns.solid('forClass'),
rdf.sym(type),
rdf.sym(indexUrl),
),
),
);
}
} else if (item && item.types.includes(ns.ldp('Resource').value)) {
let rootNode = graph.any(
null,
ns.solid('instance'),
rdf.sym(item.url),
);
if (!rootNode) {
rootNode = new rdf.NamedNode(`${indexUrl}#${cuid()}`);
ins.push(
rdf.st(
rootNode,
ns.rdf('type'),
ns.solid('TypeRegistration'),
rdf.sym(indexUrl),
),
);
ins.push(
rdf.st(
rootNode,
ns.solid('instance'),
rdf.sym(item.url),
rdf.sym(indexUrl),
),
);
item.types.forEach((type) => {
if (type)
ins.push(
rdf.st(
rootNode,
ns.solid('forClass'),
rdf.sym(type),
rdf.sym(indexUrl),
),
);
});
} else {
const currentTypes = graph
.each(rootNode, ns.solid('forClass'), null)
.map((node) => node.value);
currentTypes.map((type) => {
if (!item.types.includes(type)) {
graph
.statementsMatching(
rootNode,
ns.solid('forClass'),
rdf.sym(type),
)
.forEach((st) => {
del.push(st);
});
}
});
item.types.forEach((type) => {
if (!currentTypes.includes(type)) {
ins.push(
rdf.st(
rootNode,
ns.solid('forClass'),
rdf.sym(type),
rdf.sym(indexUrl),
),
);
}
});
}
}
});
return { del, ins };
};