feat: folder support in media library (#687)

This commit is contained in:
2023-04-11 20:51:40 +02:00
committed by GitHub
parent 49507d0b17
commit e6d3c1535a
24 changed files with 426 additions and 111 deletions

View File

@ -188,7 +188,8 @@ export default class API {
// doesn't.)
...(file.commit && file.commit.hash ? { id: this.getFileId(file.commit.hash, file.path) } : {}),
});
processFiles = (files: BitBucketFile[]) => files.filter(this.isFile).map(this.processFile);
processFiles = (files: BitBucketFile[], folderSupport?: boolean) =>
files.filter(file => (!folderSupport ? this.isFile(file) : true)).map(this.processFile);
readFile = async (
path: string,
@ -294,7 +295,7 @@ export default class API {
})),
])((cursor.data?.links as Record<string, unknown>)[action]);
listAllFiles = async (path: string, depth: number, branch: string) => {
listAllFiles = async (path: string, depth: number, branch: string, folderSupport?: boolean) => {
const { cursor: initialCursor, entries: initialEntries } = await this.listFiles(
path,
depth,
@ -311,7 +312,7 @@ export default class API {
entries.push(...newEntries);
currentCursor = newCursor;
}
return this.processFiles(entries);
return this.processFiles(entries, folderSupport);
};
async uploadFiles(

View File

@ -351,12 +351,18 @@ export default class BitbucketBackend implements BackendClass {
}));
}
async getMedia(mediaFolder = this.mediaFolder) {
async getMedia(mediaFolder = this.mediaFolder, folderSupport?: boolean) {
if (!mediaFolder) {
return [];
}
return this.api!.listAllFiles(mediaFolder, 1, this.branch).then(files =>
files.map(({ id, name, path }) => ({ id, name, path, displayURL: { id, path } })),
return this.api!.listAllFiles(mediaFolder, 1, this.branch, folderSupport).then(files =>
files.map(({ id, name, path, type }) => ({
id,
name,
path,
displayURL: { id, path },
isDirectory: type === 'commit_directory',
})),
);
}

View File

@ -391,8 +391,8 @@ export default class GitGateway implements BackendClass {
return client.enabled && client.matchPath(path);
}
getMedia(mediaFolder = this.mediaFolder) {
return this.backend!.getMedia(mediaFolder);
getMedia(mediaFolder = this.mediaFolder, folderSupport?: boolean) {
return this.backend!.getMedia(mediaFolder, folderSupport);
}
// this method memoizes this._getLargeMediaClient so that there can

View File

@ -323,6 +323,7 @@ export default class API {
async listFiles(
path: string,
{ repoURL = this.repoURL, branch = this.branch, depth = 1 } = {},
folderSupport?: boolean,
): Promise<{ type: string; id: string; name: string; path: string; size: number }[]> {
const folder = trim(path, '/');
try {
@ -336,10 +337,11 @@ export default class API {
);
return (
result.tree
// filter only files and up to the required depth
// filter only files and/or folders up to the required depth
.filter(
file =>
file.type === 'blob' && decodeURIComponent(file.path).split('/').length <= depth,
(!folderSupport ? file.type === 'blob' : true) &&
decodeURIComponent(file.path).split('/').length <= depth,
)
.map(file => ({
type: file.type,

View File

@ -281,5 +281,49 @@ describe('gitea API', () => {
params: { recursive: 1 },
});
});
it('should get files and folders', async () => {
const api = new API({ branch: 'master', repo: 'owner/repo' });
const tree = [
{
path: 'image.png',
type: 'blob',
},
{
path: 'dir1',
type: 'tree',
},
{
path: 'dir1/nested-image.png',
type: 'blob',
},
{
path: 'dir1/dir2',
type: 'tree',
},
{
path: 'dir1/dir2/nested-image.png',
type: 'blob',
},
];
api.request = jest.fn().mockResolvedValue({ tree });
await expect(api.listFiles('media', {}, true)).resolves.toEqual([
{
path: 'media/image.png',
type: 'blob',
name: 'image.png',
},
{
path: 'media/dir1',
type: 'tree',
name: 'dir1',
},
]);
expect(api.request).toHaveBeenCalledTimes(1);
expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:media', {
params: {},
});
});
});
});

View File

@ -285,15 +285,13 @@ export default class Gitea implements BackendClass {
.catch(() => ({ file: { path, id: null }, data: '' }));
}
async getMedia(mediaFolder = this.mediaFolder) {
async getMedia(mediaFolder = this.mediaFolder, folderSupport?: boolean) {
if (!mediaFolder) {
return [];
}
return this.api!.listFiles(mediaFolder).then(files =>
files.map(({ id, name, size, path }) => {
// load media using getMediaDisplayURL to avoid token expiration with Gitlab raw content urls
// for private repositories
return { id, name, size, displayURL: { id, path }, path };
return this.api!.listFiles(mediaFolder, undefined, folderSupport).then(files =>
files.map(({ id, name, size, path, type }) => {
return { id, name, size, displayURL: { id, path }, path, isDirectory: type === 'tree' };
}),
);
}

View File

@ -338,6 +338,7 @@ export default class API {
async listFiles(
path: string,
{ repoURL = this.repoURL, branch = this.branch, depth = 1 } = {},
folderSupport?: boolean,
): Promise<{ type: string; id: string; name: string; path: string; size: number }[]> {
const folder = trim(path, '/');
try {
@ -351,8 +352,12 @@ export default class API {
);
return (
result.tree
// filter only files and up to the required depth
.filter(file => file.type === 'blob' && file.path.split('/').length <= depth)
// filter only files and/or folders up to the required depth
.filter(
file =>
(!folderSupport ? file.type === 'blob' : true) &&
file.path.split('/').length <= depth,
)
.map(file => ({
type: file.type,
id: file.sha,

View File

@ -314,5 +314,49 @@ describe('github API', () => {
params: { recursive: 1 },
});
});
it('should get files and folders', async () => {
const api = new API({ branch: 'master', repo: 'owner/repo' });
const tree = [
{
path: 'image.png',
type: 'blob',
},
{
path: 'dir1',
type: 'tree',
},
{
path: 'dir1/nested-image.png',
type: 'blob',
},
{
path: 'dir1/dir2',
type: 'tree',
},
{
path: 'dir1/dir2/nested-image.png',
type: 'blob',
},
];
api.request = jest.fn().mockResolvedValue({ tree });
await expect(api.listFiles('media', {}, true)).resolves.toEqual([
{
path: 'media/image.png',
type: 'blob',
name: 'image.png',
},
{
path: 'media/dir1',
type: 'tree',
name: 'dir1',
},
]);
expect(api.request).toHaveBeenCalledTimes(1);
expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:media', {
params: {},
});
});
});
});

View File

@ -314,15 +314,15 @@ export default class GitHub implements BackendClass {
.catch(() => ({ file: { path, id: null }, data: '' }));
}
async getMedia(mediaFolder = this.mediaFolder) {
async getMedia(mediaFolder = this.mediaFolder, folderSupport?: boolean) {
if (!mediaFolder) {
return [];
}
return this.api!.listFiles(mediaFolder).then(files =>
files.map(({ id, name, size, path }) => {
return this.api!.listFiles(mediaFolder, undefined, folderSupport).then(files =>
files.map(({ id, name, size, path, type }) => {
// load media using getMediaDisplayURL to avoid token expiration with GitHub raw content urls
// for private repositories
return { id, name, size, displayURL: { id, path }, path };
return { id, name, size, displayURL: { id, path }, path, isDirectory: type == 'tree' };
}),
);
}

View File

@ -318,7 +318,12 @@ export default class API {
};
};
listAllFiles = async (path: string, recursive = false, branch = this.branch) => {
listAllFiles = async (
path: string,
folderSupport?: boolean,
recursive = false,
branch = this.branch,
) => {
const entries = [];
// eslint-disable-next-line prefer-const
let { cursor, entries: initialEntries } = await this.fetchCursorAndEntries({
@ -333,7 +338,7 @@ export default class API {
entries.push(...newEntries);
cursor = newCursor;
}
return entries.filter(({ type }) => type === 'blob');
return entries.filter(({ type }) => (!folderSupport ? type === 'blob' : true));
};
toBase64 = (str: string) => Promise.resolve(Base64.encode(str));
@ -421,7 +426,7 @@ export default class API {
for (const item of items.filter(i => i.oldPath && i.action === CommitAction.MOVE)) {
const sourceDir = dirname(item.oldPath as string);
const destDir = dirname(item.path);
const children = await this.listAllFiles(sourceDir, true, branch);
const children = await this.listAllFiles(sourceDir, undefined, true, branch);
children
.filter(f => f.path !== item.oldPath)
.forEach(file => {

View File

@ -172,7 +172,7 @@ export default class GitLab implements BackendClass {
}
async listAllFiles(folder: string, extension: string, depth: number) {
const files = await this.api!.listAllFiles(folder, depth > 1);
const files = await this.api!.listAllFiles(folder, undefined, depth > 1);
const filtered = files.filter(file => this.filterFile(folder, file, extension, depth));
return filtered;
}
@ -217,13 +217,13 @@ export default class GitLab implements BackendClass {
}));
}
async getMedia(mediaFolder = this.mediaFolder) {
async getMedia(mediaFolder = this.mediaFolder, folderSupport?: boolean) {
if (!mediaFolder) {
return [];
}
return this.api!.listAllFiles(mediaFolder).then(files =>
files.map(({ id, name, path }) => {
return { id, name, path, displayURL: { id, name, path } };
return this.api!.listAllFiles(mediaFolder, folderSupport).then(files =>
files.map(({ id, name, path, type }) => {
return { id, name, path, displayURL: { id, name, path }, isDirectory: type === 'tree' };
}),
);
}

View File

@ -146,17 +146,23 @@ export default class ProxyBackend implements BackendClass {
});
}
async getMedia(mediaFolder = this.mediaFolder, publicFolder = this.publicFolder) {
const files: { path: string; url: string }[] = await this.request({
async getMedia(
mediaFolder = this.mediaFolder,
folderSupport?: boolean,
publicFolder = this.publicFolder,
) {
const files: { path: string; url: string; isDirectory: boolean }[] = await this.request({
action: 'getMedia',
params: { branch: this.branch, mediaFolder, publicFolder },
});
return files.map(({ url, path }) => {
const filteredFiles = folderSupport ? files : files.filter(f => !f.isDirectory);
return filteredFiles.map(({ url, path, isDirectory }) => {
const id = url;
const name = basename(path);
return { id, name, displayURL: { id, path: url }, path };
return { id, name, displayURL: { id, path: url }, path, isDirectory };
});
}