WIP: first tests for building dav endpoints

This commit is contained in:
Julien Fastré 2023-09-12 11:24:50 +02:00
parent ae9f1e3905
commit 6893c833e4
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
10 changed files with 374 additions and 0 deletions

View File

@ -0,0 +1,195 @@
<?php
namespace Chill\DocStoreBundle\Controller;
use Chill\DocStoreBundle\Dav\Response\DavResponse;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Templating\EngineInterface;
final readonly class WebdavController
{
public function __construct(
private EngineInterface $engine,
private JWTTokenManagerInterface $JWTTokenManager,
private Security $security,
private StoredObjectManagerInterface $storedObjectManager,
)
{
}
/**
* @Route("/dav/open/{uuid}")
*/
public function open(StoredObject $storedObject): Response
{
/*$accessToken = $this->JWTTokenManager->createFromPayload($this->security->getUser(), [
'UserCanWrite' => true,
'UserCanAttend' => true,
'UserCanPresent' => true,
'fileId' => $storedObject->getUuid(),
]);*/
return new DavResponse($this->engine->render('@ChillDocStore/Webdav/open_in_browser.html.twig', [
'stored_object' => $storedObject, 'access_token' => '',
]));
}
/**
* @Route("/dav/get/{uuid}/", methods={"GET", "HEAD"}, name="chill_docstore_dav_directory_get")
*/
public function getDirectory(StoredObject $storedObject): Response
{
return new DavResponse(
$this->engine->render('@ChillDocStore/Webdav/directory.html.twig', [
'stored_object' => $storedObject
])
);
}
/**
* @Route("/dav/get/{uuid}/", methods={"OPTIONS"})
*/
public function optionsDirectory(StoredObject $storedObject): Response
{
$response = (new DavResponse(""))
->setEtag($this->storedObjectManager->etag($storedObject))
;
$response->headers->add(['Allow' => /*sprintf(
'%s, %s, %s, %s, %s',
Request::METHOD_OPTIONS,
Request::METHOD_GET,
Request::METHOD_HEAD,
Request::METHOD_PUT,
'PROPFIND'
) */ 'OPTIONS, GET, HEAD, DELETE, PROPFIND, PUT, PROPPATCH, COPY, MOVE, REPORT, PATCH
'
]);
$response->headers->add(['X-Tagada' => 'toin']);
return $response;
}
/**
* @Route("/dav/get/{uuid}/", methods={"PROPFIND"})
*/
public function propfindDirectory(StoredObject $storedObject): Response
{
$lastModified = $this->storedObjectManager->getLastModified($storedObject);
$etag = $this->storedObjectManager->etag($storedObject);
$length = $this->storedObjectManager->getContentLength($storedObject);
$response = new DavResponse(
$this->engine->render('@ChillDocStore/Webdav/directory_propfind.xml.twig', [
'stored_object' => $storedObject,
'last_modified' => $lastModified,
'etag' => $etag,
'content_length' => $length,
]),
207
);
$response->headers->replace([
'Content-Type' => 'text/xml'
]);
return $response;
}
/**
* @Route("/dav/get/{uuid}/d", name="chill_docstore_dav_document_get", methods={"GET"})
*/
public function getDocument(StoredObject $storedObject): Response
{
return (new DavResponse($this->storedObjectManager->read($storedObject)))
->setEtag($this->storedObjectManager->etag($storedObject));
}
/**
* @Route("/dav/get/{uuid}/d", methods={"HEAD"})
*/
public function headDocument(StoredObject $storedObject): Response
{
return (new DavResponse(""))
->setEtag($this->storedObjectManager->etag($storedObject));
}
/**
* @Route("/dav/get/{uuid}/d", methods={"OPTIONS"})
*/
public function optionsDocument(StoredObject $storedObject): Response
{
$response = (new DavResponse(""))
->setEtag($this->storedObjectManager->etag($storedObject))
;
$response->headers->add(['Allow' => /*sprintf(
'%s, %s, %s, %s, %s',
Request::METHOD_OPTIONS,
Request::METHOD_GET,
Request::METHOD_HEAD,
Request::METHOD_PUT,
'PROPFIND'
) */ 'OPTIONS, GET, HEAD, DELETE, PROPFIND, PUT, PROPPATCH, COPY, MOVE, REPORT, PATCH
'
]);
$response->headers->add(['X-Tagada' => 'toin']);
return $response;
}
/**
* @Route("/dav/get/{uuid}/d", methods={"PROPFIND"})
*/
public function propfindDocument(StoredObject $storedObject, Request $request): Response
{
$content = $request->getContent();
$xml = new \DOMDocument();
$xml->loadXml($content);
dump($xml);
$lastModified = $this->storedObjectManager->getLastModified($storedObject);
$etag = $this->storedObjectManager->etag($storedObject);
$length = $this->storedObjectManager->getContentLength($storedObject);
$response = new DavResponse(
$this->engine->render(
'@ChillDocStore/Webdav/doc_props.xml.twig',
[
'stored_object' => $storedObject,
'etag' => $etag,
'last_modified' => $lastModified,
'content_length' => $length,
]
),
207
);
$response
->headers->replace([
'Content-Type' => 'text/xml'
]);
return $response;
}
/**
* @Route("/dav/get/{uuid}/d", methods={"PUT"})
*/
public function putDocument(StoredObject $storedObject, Request $request): Response
{
dump(substr($request->getContent(), 0, 500));
return (new DavResponse(""))
->setEtag($this->storedObjectManager->etag());
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Chill\DocStoreBundle\Dav\Response;
use Symfony\Component\HttpFoundation\Response;
class DavResponse extends Response
{
public function __construct($content = '', int $status = 200, array $headers = [])
{
parent::__construct($content, $status, $headers);
$this->headers->add(['DAV' => '1']);
}
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory for {{ stored_object.uuid }}</title>
</head>
<body>
<ul>
<li><a href="{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid })) }}">d</a></li>
</ul>
</body>
</html>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" ?>
<d:multistatus xmlns:d="DAV:">
<d:response>
<d:href>{{ path('chill_docstore_dav_directory_get', { 'uuid': stored_object.uuid } ) }}</d:href>
<d:propstat>
<d:prop>
<d:getlastmodified>{{ last_modified.format(constant('DATE_ISO8601')) }}</d:getlastmodified>
<d:resourcetype>
<d:collection/>
</d:resourcetype>
<d:quota-used-bytes>57942738233</d:quota-used-bytes>
<d:quota-available-bytes>-3</d:quota-available-bytes>
<d:getetag>"{{ etag }}1"</d:getetag>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
<d:response>
<d:href>{{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid}) }}</d:href>
<d:propstat>
<d:prop>
<d:getlastmodified>Wed, 04 Sep 2019 19:02:48 GMT</d:getlastmodified>
<d:getcontentlength>{{ content_length }}</d:getcontentlength>
<d:resourcetype/>
<d:getetag>"{{ etag }}"</d:getetag>
<d:getcontenttype>{{ stored_object.type }}</d:getcontenttype>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" ?>
<d:multistatus xmlns:d="DAV:">
<d:response>
<d:href>{{ path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid}) }}</d:href>
<d:propstat>
<d:prop>
<d:getlastmodified>Wed, 04 Sep 2019 19:02:48 GMT</d:getlastmodified>
<d:getcontentlength>{{ content_length }}</d:getcontentlength>
<d:resourcetype/>
<d:getetag>"{{ etag }}"</d:getetag>
<d:getcontenttype>{{ stored_object.type }}</d:getcontenttype>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>

View File

@ -0,0 +1,7 @@
{% extends '@ChillMain/layout.html.twig' %}
{% block content %}
<p>document uuid: {{ stored_object.uuid }}</p>
<p>{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid })) }}</p>
<a href="vnd.libreoffice.command:ofe|u|{{ absolute_url(path('chill_docstore_dav_document_get', {'uuid': stored_object.uuid })) }}">Open document</a>
{% endblock %}

View File

@ -73,6 +73,62 @@ final class StoredObjectManager implements StoredObjectManagerInterface
return $this->extractLastModifiedFromResponse($response);
}
public function getContentLength(StoredObject $document): int
{
if ([] === $document->getKeyInfos()) {
if ($this->hasCache($document)) {
$response = $this->getResponseFromCache($document);
} else {
try {
$response = $this
->client
->request(
Request::METHOD_HEAD,
$this
->tempUrlGenerator
->generate(
Request::METHOD_HEAD,
$document->getFilename()
)
->url
);
} catch (TransportExceptionInterface $exception) {
throw StoredObjectManagerException::errorDuringHttpRequest($exception);
}
}
return $this->extractContentLengthFromResponse($response);
}
return strlen($this->read($document));
}
public function etag(StoredObject $document): string
{
if ($this->hasCache($document)) {
$response = $this->getResponseFromCache($document);
} else {
try {
$response = $this
->client
->request(
Request::METHOD_HEAD,
$this
->tempUrlGenerator
->generate(
Request::METHOD_HEAD,
$document->getFilename()
)
->url
);
} catch (TransportExceptionInterface $exception) {
throw StoredObjectManagerException::errorDuringHttpRequest($exception);
}
}
return $this->extractEtagFromResponse($response);
}
public function read(StoredObject $document): string
{
$response = $this->getResponseFromCache($document);
@ -163,6 +219,22 @@ final class StoredObjectManager implements StoredObjectManagerInterface
return $date;
}
private function extractContentLengthFromResponse(ResponseInterface $response): int
{
return (((int) $response->getHeaders()['content-length'] ?? [])[0] ?? 0);
}
private function extractEtagFromResponse(ResponseInterface $response): ?string
{
$etag = (($response->getHeaders()['etag'] ?? [])[0] ?? '');
if ('' === $etag) {
return md5($this->extractEtagFromResponse($response));
}
return $etag;
}
private function fillCache(StoredObject $document): void
{
try {

View File

@ -18,6 +18,8 @@ interface StoredObjectManagerInterface
{
public function getLastModified(StoredObject $document): DateTimeInterface;
public function getContentLength(StoredObject $document): int;
/**
* Get the content of a StoredObject.
*
@ -34,4 +36,6 @@ interface StoredObjectManagerInterface
* @param $clearContent The content to store in clear.
*/
public function write(StoredObject $document, string $clearContent): void;
public function etag(StoredObject $document): string;
}

View File

@ -0,0 +1,16 @@
### Get a document
GET http://{{ host }}/dav/get/{{ uuid }}/d
### OPTIONS on a document
OPTIONS http://{{ host }}/dav/get/{{ uuid }}/d
### HEAD ona document
HEAD http://{{ host }}/dav/get/{{ uuid }}/d
### Get the directory of a document
GET http://{{ host }}/dav/get/{{ uuid }}/
### Option the directory of a document
OPTIONS http://{{ host }}/dav/get/{{ uuid }}/

View File

@ -0,0 +1,6 @@
{
"dev": {
"host": "localhost:8001",
"uuid": "0bf3b8e7-b25b-4227-aae9-a3263af0766f"
}
}