This commit is contained in:
2024-04-21 14:42:52 +02:00
parent 4b69674ede
commit 8a25f53c99
10700 changed files with 55767 additions and 14201 deletions

View File

@@ -0,0 +1,48 @@
import { IncomingMessage, ServerResponse } from 'http'
import path from 'path'
import { getTargetPath } from '@/helper/path'
import { FileAPI } from '@/client/api'
import { Token } from '@/client/idp'
import { handleError } from '@/infra/error'
/*
This method copies a resource from a source URL to a destination URL.
Example implementation:
- Extract the source and destination paths from the headers or request body.
- Use fs.copyFile() to copy the file from the source to the destination.
- Set the response status code to 204 if successful or an appropriate error code if the source file is not found or encountered an error.
- Return the response.
*/
async function handleCopy(
req: IncomingMessage,
res: ServerResponse,
token: Token
) {
try {
const api = new FileAPI(token)
const sourceFile = await api.getByPath(decodeURIComponent(req.url))
const targetFile = await api.getByPath(
decodeURIComponent(path.dirname(getTargetPath(req)))
)
if (sourceFile.workspaceId !== targetFile.workspaceId) {
res.statusCode = 400
res.end()
return
}
const clones = await api.copy(targetFile.id, { ids: [sourceFile.id] })
await api.rename(clones[0].id, {
name: decodeURIComponent(path.basename(getTargetPath(req))),
})
res.statusCode = 204
res.end()
} catch (err) {
handleError(err, res)
}
}
export default handleCopy

View File

@@ -0,0 +1,34 @@
import { IncomingMessage, ServerResponse } from 'http'
import { FileAPI } from '@/client/api'
import { Token } from '@/client/idp'
import { handleError } from '@/infra/error'
/*
This method deletes a resource identified by the URL.
Example implementation:
- Extract the file path from the URL.
- Use fs.unlink() to delete the file.
- Set the response status code to 204 if successful or an appropriate error code if the file is not found.
- Return the response.
*/
async function handleDelete(
req: IncomingMessage,
res: ServerResponse,
token: Token
) {
try {
const api = new FileAPI(token)
const file = await api.getByPath(decodeURIComponent(req.url))
await api.delete(file.id)
res.statusCode = 204
res.end()
} catch (err) {
handleError(err, res)
}
}
export default handleDelete

View File

@@ -0,0 +1,68 @@
import fs, { createReadStream, rmSync } from 'fs'
import { IncomingMessage, ServerResponse } from 'http'
import os from 'os'
import path from 'path'
import { v4 as uuidv4 } from 'uuid'
import { FileAPI } from '@/client/api'
import { Token } from '@/client/idp'
import { handleError } from '@/infra/error'
/*
This method retrieves the content of a resource identified by the URL.
Example implementation:
- Extract the file path from the URL.
- Create a read stream from the file and pipe it to the response stream.
- Set the response status code to 200 if successful or an appropriate error code if the file is not found.
- Return the response.
*/
async function handleGet(
req: IncomingMessage,
res: ServerResponse,
token: Token
) {
try {
const api = new FileAPI(token)
const file = await api.getByPath(decodeURIComponent(req.url))
/* TODO: This should be optimized for the case when there is a range header,
only a partial file should be fetched, here we are fetching the whole file
which is not ideal. */
const outputPath = path.join(os.tmpdir(), uuidv4())
await api.downloadOriginal(file, outputPath)
const stat = fs.statSync(outputPath)
const rangeHeader = req.headers.range
if (rangeHeader) {
const [start, end] = rangeHeader.replace(/bytes=/, '').split('-')
const rangeStart = parseInt(start, 10) || 0
const rangeEnd = parseInt(end, 10) || stat.size - 1
const chunkSize = rangeEnd - rangeStart + 1
res.writeHead(206, {
'Content-Range': `bytes ${rangeStart}-${rangeEnd}/${stat.size}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunkSize.toString(),
'Content-Type': 'application/octet-stream',
})
createReadStream(outputPath, {
start: rangeStart,
end: rangeEnd,
})
.pipe(res)
.on('finish', () => rmSync(outputPath))
} else {
res.writeHead(200, {
'Content-Length': stat.size.toString(),
'Content-Type': 'application/octet-stream',
})
createReadStream(outputPath)
.pipe(res)
.on('finish', () => rmSync(outputPath))
}
} catch (err) {
handleError(err, res)
}
}
export default handleGet

View File

@@ -0,0 +1,37 @@
import { IncomingMessage, ServerResponse } from 'http'
import { FileAPI, FileType } from '@/client/api'
import { Token } from '@/client/idp'
import { handleError } from '@/infra/error'
/*
This method is similar to GET but only retrieves the metadata of a resource, without returning the actual content.
Example implementation:
- Extract the file path from the URL.
- Retrieve the file metadata using fs.stat().
- Set the response status code to 200 if successful or an appropriate error code if the file is not found.
- Set the Content-Length header with the file size.
- Return the response.
*/
async function handleHead(
req: IncomingMessage,
res: ServerResponse,
token: Token
) {
try {
const file = await new FileAPI(token).getByPath(decodeURIComponent(req.url))
if (file.type === FileType.File) {
res.statusCode = 200
res.setHeader('Content-Length', file.original.size)
res.end()
} else {
res.statusCode = 200
res.end()
}
} catch (err) {
handleError(err, res)
}
}
export default handleHead

View File

@@ -0,0 +1,39 @@
import { IncomingMessage, ServerResponse } from 'http'
import path from 'path'
import { File, FileAPI } from '@/client/api'
import { Token } from '@/client/idp'
import { handleError } from '@/infra/error'
/*
This method creates a new collection (directory) at the specified URL.
Example implementation:
- Extract the directory path from the URL.
- Use fs.mkdir() to create the directory.
- Set the response status code to 201 if created or an appropriate error code if the directory already exists or encountered an error.
- Return the response.
*/
async function handleMkcol(
req: IncomingMessage,
res: ServerResponse,
token: Token
) {
let directory: File
try {
const api = new FileAPI(token)
directory = await api.getByPath(decodeURIComponent(path.dirname(req.url)))
await api.createFolder({
workspaceId: directory.workspaceId,
parentId: directory.id,
name: decodeURIComponent(path.basename(req.url)),
})
res.statusCode = 201
res.end()
} catch (err) {
handleError(err, res)
}
}
export default handleMkcol

View File

@@ -0,0 +1,57 @@
import { IncomingMessage, ServerResponse } from 'http'
import path from 'path'
import { FileAPI } from '@/client/api'
import { Token } from '@/client/idp'
import { getTargetPath } from '@/helper/path'
import { handleError } from '@/infra/error'
/*
This method moves or renames a resource from a source URL to a destination URL.
Example implementation:
- Extract the source and destination paths from the headers or request body.
- Use fs.rename() to move or rename the file from the source to the destination.
- Set the response status code to 204 if successful or an appropriate error code if the source file is not found or encountered an error.
- Return the response.
*/
async function handleMove(
req: IncomingMessage,
res: ServerResponse,
token: Token
) {
try {
const sourcePath = decodeURIComponent(req.url)
const targetPath = decodeURIComponent(getTargetPath(req))
const api = new FileAPI(token)
const sourceFile = await api.getByPath(decodeURIComponent(req.url))
const targetFile = await api.getByPath(
decodeURIComponent(path.dirname(getTargetPath(req)))
)
if (sourceFile.workspaceId !== targetFile.workspaceId) {
res.statusCode = 400
res.end()
return
}
if (
sourcePath.split('/').length === targetPath.split('/').length &&
path.dirname(sourcePath) === path.dirname(targetPath)
) {
await api.rename(sourceFile.id, {
name: decodeURIComponent(path.basename(targetPath)),
})
} else {
await api.move(targetFile.id, { ids: [sourceFile.id] })
}
res.statusCode = 204
res.end()
} catch (err) {
handleError(err, res)
}
}
export default handleMove

View File

@@ -0,0 +1,21 @@
import { IncomingMessage, ServerResponse } from 'http'
/*
This method should respond with the allowed methods and capabilities of the server.
Example implementation:
- Set the response status code to 200.
- Set the Allow header to specify the supported methods, such as OPTIONS, GET, PUT, DELETE, etc.
- Return the response.
*/
async function handleOptions(_: IncomingMessage, res: ServerResponse) {
res.statusCode = 200
res.setHeader(
'Allow',
'OPTIONS, GET, HEAD, PUT, DELETE, MKCOL, COPY, MOVE, PROPFIND, PROPPATCH'
)
res.end()
}
export default handleOptions

View File

@@ -0,0 +1,111 @@
import { IncomingMessage, ServerResponse } from 'http'
import { FileAPI, FileType } from '@/client/api'
import { Token } from '@/client/idp'
import { handleError } from '@/infra/error'
/*
This method retrieves properties and metadata of a resource.
Example implementation:
- Extract the file path from the URL.
- Use fs.stat() to retrieve the file metadata.
- Format the response body in the desired XML format with the properties and metadata.
- Set the response status code to 207 if successful or an appropriate error code if the file is not found or encountered an error.
- Set the Content-Type header to indicate the XML format.
- Return the response.
*/
async function handlePropfind(
req: IncomingMessage,
res: ServerResponse,
token: Token
) {
try {
const api = new FileAPI(token)
const file = await api.getByPath(decodeURIComponent(req.url))
if (file.type === FileType.File) {
const responseXml = `
<D:multistatus xmlns:D="DAV:">
<D:response>
<D:href>${encodeURIComponent(file.name)}</D:href>
<D:propstat>
<D:prop>
<D:resourcetype></D:resourcetype>
${
file.original
? `<D:getcontentlength>${file.original.size}</D:getcontentlength>`
: ''
}
<D:creationdate>${new Date(
file.createTime
).toUTCString()}</D:creationdate>
<D:getlastmodified>${new Date(
file.updateTime
).toUTCString()}</D:getlastmodified>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>`
res.statusCode = 207
res.setHeader('Content-Type', 'application/xml; charset=utf-8')
res.end(responseXml)
} else if (file.type === FileType.Folder) {
const list = await api.listByPath(decodeURIComponent(req.url))
const responseXml = `
<D:multistatus xmlns:D="DAV:">
<D:response>
<D:href>${req.url}</D:href>
<D:propstat>
<D:prop>
<D:resourcetype><D:collection/></D:resourcetype>
<D:getcontentlength>0</D:getcontentlength>
<D:getlastmodified>${new Date(
file.updateTime
).toUTCString()}</D:getlastmodified>
<D:creationdate>${new Date(
file.createTime
).toUTCString()}</D:creationdate>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
${list
.map((item) => {
return `
<D:response>
<D:href>${req.url}${encodeURIComponent(item.name)}</D:href>
<D:propstat>
<D:prop>
<D:resourcetype>${
item.type === FileType.Folder ? '<D:collection/>' : ''
}</D:resourcetype>
${
item.type === FileType.File && item.original
? `<D:getcontentlength>${item.original.size}</D:getcontentlength>`
: ''
}
<D:getlastmodified>${new Date(
item.updateTime
).toUTCString()}</D:getlastmodified>
<D:creationdate>${new Date(
item.createTime
).toUTCString()}</D:creationdate>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
`
})
.join('')}
</D:multistatus>`
res.statusCode = 207
res.setHeader('Content-Type', 'application/xml; charset=utf-8')
res.end(responseXml)
}
} catch (err) {
handleError(err, res)
}
}
export default handlePropfind

View File

@@ -0,0 +1,34 @@
import { IncomingMessage, ServerResponse } from 'http'
/*
This method updates the properties of a resource.
Example implementation:
- Parse the request body to extract the properties to be updated.
- Read the existing data from the file.
- Parse the existing properties.
- Merge the updated properties with the existing ones.
- Format the updated properties and store them back in the file.
- Set the response status code to 204 if successful or an appropriate error code if the file is not found or encountered an error.
- Return the response.
In this example implementation, the handleProppatch() method first parses the XML
payload containing the properties to be updated. Then, it reads the existing data from the file,
parses the existing properties (assuming an XML format),
merges the updated properties with the existing ones, and formats
the properties back into the desired format (e.g., XML).
Finally, the updated properties are written back to the file.
You can customize the parseProperties() and formatProperties()
functions to match the specific property format you are using in your WebDAV server.
Note that this implementation assumes a simplified example and may require further
customization based on your specific property format and requirements.
*/
async function handleProppatch(_: IncomingMessage, res: ServerResponse) {
res.statusCode = 501
res.end()
}
export default handleProppatch

View File

@@ -0,0 +1,71 @@
import fs from 'fs'
import { readFile } from 'fs/promises'
import { IncomingMessage, ServerResponse } from 'http'
import os from 'os'
import path from 'path'
import { v4 as uuidv4 } from 'uuid'
import { Token } from '@/client/idp'
import { File, FileAPI } from '@/client/api'
import { handleError } from '@/infra/error'
/*
This method creates or updates a resource with the provided content.
Example implementation:
- Extract the file path from the URL.
- Create a write stream to the file.
- Listen for the data event to write the incoming data to the file.
- Listen for the end event to indicate the completion of the write stream.
- Set the response status code to 201 if created or 204 if updated.
- Return the response.
*/
async function handlePut(
req: IncomingMessage,
res: ServerResponse,
token: Token
) {
const api = new FileAPI(token)
try {
const directory = await api.getByPath(
decodeURIComponent(path.dirname(req.url))
)
const outputPath = path.join(os.tmpdir(), uuidv4())
const ws = fs.createWriteStream(outputPath)
req.pipe(ws)
ws.on('error', (err) => {
console.error(err)
res.statusCode = 500
res.end()
})
ws.on('finish', async () => {
try {
res.statusCode = 201
res.end()
const blob = new Blob([await readFile(outputPath)])
let existingFile: File | null = null
try {
existingFile = await api.getByPath(decodeURIComponent(req.url))
// Delete existing file to simulate an overwrite
await api.delete(existingFile.id)
} catch {
// Ignored
}
await api.upload({
workspaceId: directory.workspaceId,
parentId: directory.id,
name: decodeURIComponent(path.basename(req.url)),
blob,
})
} catch (err) {
handleError(err, res)
} finally {
fs.rmSync(outputPath)
}
})
} catch (err) {
handleError(err, res)
}
}
export default handlePut