Skip to content
Snippets Groups Projects
Commit 5e279430 authored by Stefan Hynek's avatar Stefan Hynek :drooling_face:
Browse files

Merge branch '9-integrate-with-tgclients-lib' into 'main'

Resolve "integrate with tgclients lib"

Closes #9

See merge request !8
parents 8bc370b8 65b9f257
No related branches found
No related tags found
1 merge request!8Resolve "integrate with tgclients lib"
Pipeline #276347 passed
{
"configurations": [
{
"name": "Python: main.py",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/src/main.py",
"justMyCode": false
}
]
}
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2009-2022 Martin Wendt and contributors <https://github.com/mar10/wsgidav>
# SPDX-FileCopyrightText: 2022 Stefan Hynek
#
# SPDX-License-Identifier: MIT
#
# Original file: https://github.com/mar10/wsgidav/blob/d22ada5db70812ac9201c6861c53ce5cf2157342/wsgidav/stream_tools.py
"""Implement the FileLikeQueue helper class.
This helper class is intended to handle use cases where an incoming PUT
request should be directly streamed to a remote target.
Usage: return an instance of this class to`begin_write` and pass it to the
consumer at the same time::
def begin_write(self, contentType=None):
queue = FileLikeQueue(length)
requests.post(..., data=queue)
return queue
"""
import logging
import queue
_logger = logging.getLogger(__name__)
# ============================================================================
# FileLikeQueue
# ============================================================================
class FileLikeQueue:
"""A queue for chunks that behaves like a file-like.
read() and write() are typically called from different threads.
This helper class is intended to handle use cases where an incoming PUT
request should be directly streamed to a remote target:
def begin_write(self, contentType=None):
# Create a proxy buffer
queue = FileLikeQueue(length)
# ... and use it as source for the consumer:
requests.post(..., data=queue)
# pass it to the PUT handler as target
return queue
"""
def __init__(self, length: int, max_size: int=0):
self.is_closed = False
self.queue = queue.Queue(max_size)
self.unread = b""
self.len = length
def __len__(self) -> int:
_logger.debug(
"Called FileLikeQueue.__len__(self). Returned %s", self.len)
return self.len
#: File API
def read(self, size: int=0) -> bytes:
"""Read a chunk of bytes from queue.
This method blocks until the requested size become available.
However, if close() was called, '' is returned immediately.
Args:
size: Chunk length.
size = 0: Read next chunk (arbitrary length)
> 0: Read one chunk of `size` bytes (or less if stream was closed)
< 0: Read all bytes as single chunk (i.e. blocks until stream is closed)
Returns:
Chunk in bytes.
"""
_logger.debug(
"Called FileLikeQueue.read(self, size=%s).", size)
res = self.unread
self.unread = b""
# Get next chunk, cumulating requested size as needed
while res == b"" or size < 0 or (size > 0 and len(res) < size):
try:
# Read pending data, blocking if neccessary
# (but handle the case that close() is called while waiting)
res += self.queue.get(True, 0.1)
except queue.Empty:
# There was no pending data: wait for more, unless close() was called
if self.is_closed:
break
# Deliver `size` bytes from buffer
if len(res) > size > 0:
self.unread = res[size:]
res = res[:size]
# Reduce the Queue's length by the number of bytes read
self.len -= len(res)
return res
def write(self, chunk: bytes):
"""Put a chunk of bytes (or an iterable) to the queue.
May block if max_size number of chunks is reached.
Args:
chunk: A chunk of bytes or an iterable
"""
_logger.debug(
"Called FileLikeQueue.write(self, chunk=%s).", len(chunk))
if self.is_closed:
raise ValueError("Cannot write to closed object")
# Add chunk to queue (blocks if queue is full)
if isinstance(chunk, (str, bytes)):
self.queue.put(chunk)
else: # if not a string, assume an iterable
for o in chunk:
self.queue.put(o)
def close(self):
"""Close the Queue for incoming data.
"""
_logger.debug("Called FileLikeQueue.close(self).")
self.is_closed = True
#: Iterator API
def __iter__(self):
_logger.debug("Called FileLikeQueue.__iter__(self).")
return self
def __next__(self):
_logger.debug("Called FileLikeQueue.__next__(self).")
result = self.read()
if not result:
raise StopIteration
return result
next = __next__ # Python 2.x
...@@ -2,18 +2,22 @@ ...@@ -2,18 +2,22 @@
""" """
import io import io
import logging import logging
import threading
from pprint import pformat
from tgclients.auth import TextgridAuth
from tgclients.config import TextgridConfig
from tgclients.crud import TextgridCRUD
from wsgidav.dav_provider import DAVCollection, DAVNonCollection, DAVProvider from wsgidav.dav_provider import DAVCollection, DAVNonCollection, DAVProvider
from wsgidav.util import join_uri, pop_path from wsgidav.util import join_uri, pop_path
from repdav.stream_tools import FileLikeQueue
from .tgapi import TextgridAuth, TextgridCRUD, TextgridSearch from .tgapi import TextgridSearch
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
# TODO think about caching with the session ID as key
class TextgridRoot(DAVCollection): class TextgridRoot(DAVCollection):
"""Top level collection that incorporates Textgrid projects. """Top level collection that incorporates Textgrid projects.
...@@ -23,14 +27,15 @@ class TextgridRoot(DAVCollection): ...@@ -23,14 +27,15 @@ class TextgridRoot(DAVCollection):
def __init__(self, path, environ): def __init__(self, path, environ):
DAVCollection.__init__(self, path, environ) DAVCollection.__init__(self, path, environ)
self._sid = environ["wsgidav.auth.user_name"] self._sid = environ["wsgidav.auth.user_name"]
_logger.debug("MY SID: %s", self._sid)
def get_display_info(self): def get_display_info(self):
return {"type": "Textgrid root collection"} return {"type": "Textgrid root collection"}
def get_member_names(self): def get_member_names(self):
_logger.debug("Called TextgridRoot.get_member_names(self).") _logger.debug("Called TextgridRoot.get_member_names(self).")
projects = tuple(TextgridAuth().assigned_projects(self._sid)) config = TextgridConfig()
auth = TextgridAuth(config)
projects = tuple(auth.list_assigned_projects(self._sid))
_logger.debug("MY PROJECTS: %s", projects) _logger.debug("MY PROJECTS: %s", projects)
return projects return projects
...@@ -38,20 +43,32 @@ class TextgridRoot(DAVCollection): ...@@ -38,20 +43,32 @@ class TextgridRoot(DAVCollection):
_logger.debug("Called TextgridRoot.get_member(self, %s).", name) _logger.debug("Called TextgridRoot.get_member(self, %s).", name)
return TextgridProject(join_uri(self.path, name), self.environ) return TextgridProject(join_uri(self.path, name), self.environ)
def support_etag(self):
"""Return True, if this resource supports ETags."""
return False
def get_etag(self):
"""
See http://www.webdav.org/specs/rfc4918.html#PROPERTY_getetag
This method SHOULD be implemented, especially by non-collections.
"""
return None
# temporary override for debugging # temporary override for debugging
def resolve(self, script_name, path_info): def resolve(self, script_name, path_info):
"""Return a _DAVResource object for the path (None, if not found). """Return a _DAVResource object for the path (None, if not found).
`path_info`: is a URL relative to this object. `path_info`: is a URL relative to this object.
""" """
_logger.debug("Called TextgridRoot.resolve(self, %s, %s).", _logger.debug(
script_name, path_info) "Called TextgridRoot.resolve(self, %s, %s).", script_name, path_info
)
if path_info in ("", "/"): if path_info in ("", "/"):
return self return self
assert path_info.startswith("/") assert path_info.startswith("/")
name, rest = pop_path(path_info) name, rest = pop_path(path_info)
res = self.get_member(name) res = self.get_member(name)
_logger.debug("TextgridRoot_NAME: %s, REST: %s, RES: %s", _logger.debug("TextgridRoot_NAME: %s, REST: %s, RES: %s", name, rest, res)
name, rest, res)
if res is None or rest in ("", "/"): if res is None or rest in ("", "/"):
return res return res
_logger.debug("RES: %s", res) _logger.debug("RES: %s", res)
...@@ -60,8 +77,7 @@ class TextgridRoot(DAVCollection): ...@@ -60,8 +77,7 @@ class TextgridRoot(DAVCollection):
class TextgridProject(DAVCollection): class TextgridProject(DAVCollection):
def __init__(self, path, environ): def __init__(self, path, environ):
_logger.debug( _logger.debug("Called TextgridProject.__init__(self, %s, environ).", path)
"Called TextgridProject.__init__(self, %s, environ).", path)
DAVCollection.__init__(self, path, environ) DAVCollection.__init__(self, path, environ)
self._sid = environ["wsgidav.auth.user_name"] self._sid = environ["wsgidav.auth.user_name"]
...@@ -84,8 +100,8 @@ class TextgridProject(DAVCollection): ...@@ -84,8 +100,8 @@ class TextgridProject(DAVCollection):
# #
# path resolution has to be rewritten before we can work with resource titles # path resolution has to be rewritten before we can work with resource titles
resources = TextgridSearch().get_project_contents( resources = TextgridSearch().get_project_contents(
self._sid, self.path.split("/")[-1]) self._sid, self.path.split("/")[-1]
_logger.debug("RESOURCES: %s", resources) )
return resources.keys() return resources.keys()
def get_member(self, name): def get_member(self, name):
...@@ -102,31 +118,46 @@ class TextgridProject(DAVCollection): ...@@ -102,31 +118,46 @@ class TextgridProject(DAVCollection):
def copy_move_single(self, dest_path, is_move): def copy_move_single(self, dest_path, is_move):
pass pass
def support_etag(self):
"""Return True, if this resource supports ETags."""
return False
def get_etag(self):
"""
See http://www.webdav.org/specs/rfc4918.html#PROPERTY_getetag
This method SHOULD be implemented, especially by non-collections.
"""
return None
# temporary override for debugging # temporary override for debugging
def resolve(self, script_name, path_info): def resolve(self, script_name, path_info):
"""Return a _DAVResource object for the path (None, if not found). """Return a _DAVResource object for the path (None, if not found).
`path_info`: is a URL relative to this object. `path_info`: is a URL relative to this object.
""" """
_logger.debug( _logger.debug(
"Called TextgridProject.resolve(self, %s, %s).", script_name, path_info) "Called TextgridProject.resolve(self, %s, %s).", script_name, path_info
)
if path_info in ("", "/"): if path_info in ("", "/"):
return self return self
assert path_info.startswith("/") assert path_info.startswith("/")
name, rest = pop_path(path_info) name, rest = pop_path(path_info)
res = self.get_member(name) res = self.get_member(name)
_logger.debug( _logger.debug("TextgridProject_NAME: %s, REST: %s, RES: %s", name, rest, res)
"TextgridProject_NAME: %s, REST: %s, RES: %s", name, rest, res)
if res is None or rest in ("", "/"): if res is None or rest in ("", "/"):
return res return res
return res.resolve(join_uri(script_name, name), rest) return res.resolve(join_uri(script_name, name), rest)
# TODO: merge TextgridProject with TextgridAggregation and/or derive from a common base class.
# TODO: merge TextgridProject with TextgridAggregation
# and/or derive from a common base class.
class TextgridAggregation(DAVCollection): class TextgridAggregation(DAVCollection):
def __init__(self, path, environ, info): def __init__(self, path, environ, info):
_logger.debug( _logger.debug(
"Called TextgridAggregation.__init__(self, %s, environ).", path) "Called TextgridAggregation.__init__(self, %s, environ, info).", path
)
DAVCollection.__init__(self, path, environ) DAVCollection.__init__(self, path, environ)
self._sid = environ["wsgidav.auth.user_name"] self._sid = environ["wsgidav.auth.user_name"]
self._info = info self._info = info
...@@ -143,8 +174,9 @@ class TextgridAggregation(DAVCollection): ...@@ -143,8 +174,9 @@ class TextgridAggregation(DAVCollection):
def get_member_names(self): def get_member_names(self):
_logger.debug("Called TextgridAggregation.get_member_names(self).") _logger.debug("Called TextgridAggregation.get_member_names(self).")
resources = TextgridSearch().get_aggregation_contents( resources = TextgridSearch().get_aggregation_contents(
self._sid, self.path.split("/")[-1]) self._sid, self.path.split("/")[-1]
#_logger.debug("RESOURCES: %s", resources) )
# _logger.debug("RESOURCES: %s", resources)
return resources.keys() return resources.keys()
def get_member(self, name): def get_member(self, name):
...@@ -161,20 +193,34 @@ class TextgridAggregation(DAVCollection): ...@@ -161,20 +193,34 @@ class TextgridAggregation(DAVCollection):
def copy_move_single(self, dest_path, is_move): def copy_move_single(self, dest_path, is_move):
pass pass
def support_etag(self):
"""Return True, if this resource supports ETags."""
return False
def get_etag(self):
"""
See http://www.webdav.org/specs/rfc4918.html#PROPERTY_getetag
This method SHOULD be implemented, especially by non-collections.
"""
return None
# temporary override for debugging # temporary override for debugging
def resolve(self, script_name, path_info): def resolve(self, script_name, path_info):
"""Return a _DAVResource object for the path (None, if not found). """Return a _DAVResource object for the path (None, if not found).
`path_info`: is a URL relative to this object. `path_info`: is a URL relative to this object.
""" """
_logger.debug( _logger.debug(
"Called TextgridAggregation.resolve(self, %s, %s).", script_name, path_info) "Called TextgridAggregation.resolve(self, %s, %s).", script_name, path_info
)
if path_info in ("", "/"): if path_info in ("", "/"):
return self return self
assert path_info.startswith("/") assert path_info.startswith("/")
name, rest = pop_path(path_info) name, rest = pop_path(path_info)
res = self.get_member(name) res = self.get_member(name)
_logger.debug( _logger.debug(
"TextgridAggregation_NAME: %s, REST: %s, RES: %s", name, rest, res) "TextgridAggregation_NAME: %s, REST: %s, RES: %s", name, rest, res
)
if res is None or rest in ("", "/"): if res is None or rest in ("", "/"):
return res return res
return res.resolve(join_uri(script_name, name), rest) return res.resolve(join_uri(script_name, name), rest)
...@@ -184,42 +230,95 @@ class TextgridResource(DAVNonCollection): ...@@ -184,42 +230,95 @@ class TextgridResource(DAVNonCollection):
"""Non-Aggregation resources.""" """Non-Aggregation resources."""
def __init__(self, path, environ, info): def __init__(self, path, environ, info):
_logger.debug( _logger.debug("Called TextgridResource.__init__(self, %s, environ).", path)
"Called TextgridResource.__init__(self, %s, environ).", path)
DAVNonCollection.__init__(self, path, environ) DAVNonCollection.__init__(self, path, environ)
self._size = environ.get("CONTENT_LENGTH")
self._sid = environ["wsgidav.auth.user_name"] self._sid = environ["wsgidav.auth.user_name"]
self._info = info self._info = info
self.upload_thread = None
def get_content_length(self): def get_content_length(self):
_logger.debug("Called TextgridResource.get_content_length(self).") _logger.debug("Called TextgridResource.get_content_length(self).")
# return TextgridCRUD().get_metadata(self._sid, self.path.split("/")[-1])["content-length"]
return self._info[self.name]["extent"] return self._info[self.name]["extent"]
def get_content_type(self): def get_content_type(self):
_logger.debug("Called TextgridResource.get_content_type(self).") _logger.debug("Called TextgridResource.get_content_type(self).")
# return TextgridCRUD().get_metadata(self._sid, self.path.split("/")[-1])["content-type"]
return self._info[self.name]["format"] return self._info[self.name]["format"]
def get_content(self): def get_content(self):
return io.BytesIO(TextgridCRUD().get_data(self._sid, self.path.split("/")[-1])) config = TextgridConfig()
crud = TextgridCRUD(config.crud)
return io.BytesIO(crud.read_data(self.path.split("/")[-1], self._sid).content)
def get_content_title(self):
_logger.debug("Called TextgridResource.get_content_title(self).")
return self._info[self.name]["title"]
def begin_write(self, content_type=None): def begin_write(self, content_type=None):
pass _logger.debug(
"Called TextgridResource.begin_write(self, content_type=%s).", content_type
)
queue = FileLikeQueue(int(self._size))
config = TextgridConfig("http://textgridlab.org/")
crud = TextgridCRUD(config.crud)
metadata = crud.read_metadata(self.path.split("/")[-1], self._sid).content
def worker():
_logger.debug("Called TextgridResource.begin_write.worker().")
crud.update_resource(self._sid, self.path.split("/")[-1], queue, metadata)
thread = threading.Thread(target=worker)
thread.setDaemon(True)
thread.start()
self.upload_thread = thread
return queue
def end_write(self, with_errors):
_logger.debug(
"Called TextgridResource.end_write(self, with_errors=%s)", with_errors
)
if self.upload_thread:
self.upload_thread.join()
self.upload_thread = None
def support_etag(self):
"""Return True, if this resource supports ETags."""
return False
def get_etag(self):
"""
See http://www.webdav.org/specs/rfc4918.html#PROPERTY_getetag
This method SHOULD be implemented, especially by non-collections.
"""
return None
# temporary override for debugging
def resolve(self, script_name, path_info):
"""Return a _DAVResource object for the path (None, if not found).
`path_info`: is a URL relative to this object.
"""
_logger.debug(
"Called TextgridResource.resolve(self, %s, %s).", script_name, path_info
)
if path_info in ("", "/"):
return self
return None
# ============================================================================ # ============================================================================
# TextgridResourceProvider # TextgridResourceProvider
# ============================================================================ # ============================================================================
class TextgridResourceProvider(DAVProvider): class TextgridResourceProvider(DAVProvider):
"""DAV provider that serves Textgrid resources. """DAV provider that serves Textgrid resources."""
"""
def __init__(self):
super(TextgridResourceProvider, self).__init__()
def get_resource_inst(self, path, environ): def get_resource_inst(self, path, environ):
_logger.debug( _logger.debug(
"Called TextgridResourceProvider.get_resource_inst(self, %s, %s).", path, environ) "Called TextgridResourceProvider.get_resource_inst(self, %s, %s).",
path,
pformat(environ),
)
self._count_get_resource_inst += 1 self._count_get_resource_inst += 1
root = TextgridRoot("/", environ) root = TextgridRoot("/", environ)
# an instance of _DAVResource (i.e. either DAVCollection or DAVNonCollection) # an instance of _DAVResource (i.e. either DAVCollection or DAVNonCollection)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment