paulb@22 | 1 | #!/usr/bin/env python |
paulb@22 | 2 | |
paulb@22 | 3 | "A calendar application." |
paulb@22 | 4 | |
paulb@22 | 5 | import WebStack.Generic |
paulb@500 | 6 | from WebStack.Repositories.Directory import DirectoryRepository |
paulb@380 | 7 | import time |
paulb@490 | 8 | import os, tempfile |
paulb@22 | 9 | |
paulb@416 | 10 | class CalendarResource: |
paulb@22 | 11 | |
paulb@22 | 12 | """ |
paulb@22 | 13 | A resource which handles incoming calendars and viewing requests. |
paulb@22 | 14 | An arbitrary set of rules can be applied to determine what is to be done |
paulb@22 | 15 | with a request, and in this case, the application appears as a directory of |
paulb@22 | 16 | calendars, yet also accepts incoming calendars. |
paulb@22 | 17 | """ |
paulb@22 | 18 | |
paulb@416 | 19 | resource_dir = os.path.join(os.path.split(__file__)[0], "calendars") |
paulb@456 | 20 | encoding = "utf-8" |
paulb@416 | 21 | |
paulb@500 | 22 | dav_header = \ |
paulb@500 | 23 | """<?xml version="1.0"?> |
paulb@500 | 24 | <D:multistatus xmlns:D="DAV:"> |
paulb@500 | 25 | """ |
paulb@500 | 26 | propfind_header = \ |
paulb@500 | 27 | """<D:response> |
paulb@500 | 28 | <D:href>%s</D:href> |
paulb@500 | 29 | <D:propstat> |
paulb@500 | 30 | <D:prop> |
paulb@500 | 31 | <D:creationdate>%s</D:creationdate> |
paulb@500 | 32 | <D:displayname>%s</D:displayname> |
paulb@500 | 33 | <D:resourcetype> |
paulb@500 | 34 | <D:collection/> |
paulb@500 | 35 | </D:resourcetype> |
paulb@500 | 36 | </D:prop> |
paulb@500 | 37 | <D:status>HTTP/1.1 200 OK</D:status> |
paulb@500 | 38 | </D:propstat> |
paulb@500 | 39 | </D:response> |
paulb@500 | 40 | """ |
paulb@500 | 41 | propfind_item = \ |
paulb@500 | 42 | """<D:response> |
paulb@500 | 43 | <D:href>%s%s</D:href> |
paulb@500 | 44 | <D:propstat> |
paulb@500 | 45 | <D:prop> |
paulb@500 | 46 | <D:creationdate>%s</D:creationdate> |
paulb@500 | 47 | <D:displayname>%s</D:displayname> |
paulb@500 | 48 | <D:resourcetype/> |
paulb@500 | 49 | <D:getcontenttype>%s</D:getcontenttype> |
paulb@500 | 50 | <D:getcontentlength>%s</D:getcontentlength> |
paulb@500 | 51 | </D:prop> |
paulb@500 | 52 | <D:status>HTTP/1.1 200 OK</D:status> |
paulb@500 | 53 | </D:propstat> |
paulb@500 | 54 | </D:response> |
paulb@500 | 55 | """ |
paulb@500 | 56 | propfind_footer = \ |
paulb@500 | 57 | """</D:multistatus> |
paulb@500 | 58 | """ |
paulb@493 | 59 | |
paulb@500 | 60 | def __init__(self, fsencoding=None): |
paulb@500 | 61 | try: |
paulb@500 | 62 | self.repository = DirectoryRepository(self.resource_dir, fsencoding) |
paulb@500 | 63 | except OSError: |
paulb@500 | 64 | self.resource_dir = os.path.join(tempfile.gettempdir(), "calendars") |
paulb@500 | 65 | self.repository = DirectoryRepository(self.resource_dir, fsencoding) |
paulb@416 | 66 | |
paulb@22 | 67 | def respond(self, trans): |
paulb@22 | 68 | |
paulb@22 | 69 | """ |
paulb@22 | 70 | Examine the incoming request, either saving a calendar or displaying |
paulb@22 | 71 | one. |
paulb@22 | 72 | """ |
paulb@22 | 73 | |
paulb@22 | 74 | # Determine the action to be taken. |
paulb@22 | 75 | |
paulb@22 | 76 | method = trans.get_request_method() |
paulb@22 | 77 | |
paulb@22 | 78 | # NOTE: Some frameworks do not pass in the content type. |
paulb@416 | 79 | # NOTE: We always assume that calendar files are being uploaded. |
paulb@22 | 80 | |
paulb@456 | 81 | calendar_name = trans.get_virtual_path_info(self.encoding).split("/")[-1] |
paulb@22 | 82 | |
paulb@380 | 83 | # Handle uploads. |
paulb@380 | 84 | |
paulb@22 | 85 | if method == "PUT": |
paulb@380 | 86 | |
paulb@380 | 87 | # Get the last path component as the name of the calendar. |
paulb@380 | 88 | # NOTE: This could be improved to permit hierarchical naming. |
paulb@380 | 89 | |
paulb@22 | 90 | input = trans.get_request_stream() |
paulb@380 | 91 | data = input.read() |
paulb@380 | 92 | |
paulb@416 | 93 | # Store the calendar in the directory. |
paulb@22 | 94 | |
paulb@500 | 95 | self.repository[calendar_name] = data |
paulb@22 | 96 | |
paulb@380 | 97 | # Handle directory browsing. |
paulb@380 | 98 | |
paulb@380 | 99 | elif method == "PROPFIND": |
paulb@380 | 100 | trans.set_response_code(207) |
paulb@456 | 101 | trans.set_content_type(WebStack.Generic.ContentType("text/xml", self.encoding)) |
paulb@22 | 102 | out = trans.get_response_stream() |
paulb@500 | 103 | out.write(self.dav_header) |
paulb@380 | 104 | |
paulb@456 | 105 | if trans.get_virtual_path_info(self.encoding) == "/": |
paulb@416 | 106 | time_now = time.strftime("%Y-%m-%dT%TZ", time.gmtime(time.time())) |
paulb@500 | 107 | out.write(self.propfind_header % ( |
paulb@500 | 108 | trans.get_path_without_query(self.encoding), |
paulb@500 | 109 | time_now, |
paulb@500 | 110 | trans.get_path_without_query(self.encoding))) |
paulb@380 | 111 | |
paulb@500 | 112 | for filename in self.repository.keys(): |
paulb@500 | 113 | pathname = self.repository.full_path(filename) |
paulb@416 | 114 | created = time.strftime("%Y-%m-%dT%TZ", time.gmtime(os.path.getctime(pathname))) |
paulb@416 | 115 | size = os.path.getsize(pathname) |
paulb@500 | 116 | out.write(self.propfind_item % ( |
paulb@500 | 117 | trans.get_path_without_query(self.encoding), |
paulb@500 | 118 | filename, |
paulb@500 | 119 | created, |
paulb@500 | 120 | filename, |
paulb@500 | 121 | "text/calendar", |
paulb@500 | 122 | size)) |
paulb@380 | 123 | |
paulb@500 | 124 | out.write(self.propfind_footer) |
paulb@380 | 125 | |
paulb@380 | 126 | # Handle downloads. |
paulb@380 | 127 | |
paulb@380 | 128 | elif method == "GET": |
paulb@456 | 129 | trans.set_content_type(WebStack.Generic.ContentType("text/calendar")) |
paulb@380 | 130 | out = trans.get_response_stream() |
paulb@500 | 131 | out.write(self.repository[calendar_name]) |
paulb@416 | 132 | |
paulb@416 | 133 | # Handle deletion. |
paulb@416 | 134 | |
paulb@416 | 135 | elif method == "DELETE": |
paulb@416 | 136 | try: |
paulb@500 | 137 | del self.repository[calendar_name] |
paulb@416 | 138 | except OSError: |
paulb@416 | 139 | trans.set_response_code(500) |
paulb@416 | 140 | |
paulb@416 | 141 | # Handle renaming. |
paulb@416 | 142 | |
paulb@416 | 143 | elif method in ("MOVE", "COPY"): |
paulb@416 | 144 | destinations = trans.get_header_values("Destination") |
paulb@416 | 145 | if len(destinations) != 1: |
paulb@416 | 146 | trans.set_response_code(500) |
paulb@416 | 147 | else: |
paulb@416 | 148 | try: |
paulb@416 | 149 | # Convert the URL into a filename. |
paulb@416 | 150 | # NOTE: Assume that the URL references the same "directory". |
paulb@416 | 151 | |
paulb@416 | 152 | destination = destinations[0].split("/")[-1] |
paulb@456 | 153 | destination = trans.decode_path(destination, self.encoding) |
paulb@416 | 154 | |
paulb@500 | 155 | self.repository[destination] = self.repository[calendar_name] |
paulb@416 | 156 | if method == "MOVE": |
paulb@500 | 157 | del self.repository[calendar_name] |
paulb@416 | 158 | |
paulb@416 | 159 | # NOTE: We do not observe the rules regarding overwriting |
paulb@416 | 160 | # NOTE: and the appropriate status codes. |
paulb@416 | 161 | |
paulb@416 | 162 | trans.set_header_value("Location", destinations[0]) |
paulb@416 | 163 | trans.set_response_code(201) |
paulb@416 | 164 | |
paulb@416 | 165 | except OSError: |
paulb@416 | 166 | trans.set_response_code(500) |
paulb@380 | 167 | |
paulb@380 | 168 | # Disallow other methods. |
paulb@380 | 169 | |
paulb@380 | 170 | else: |
paulb@380 | 171 | trans.set_response_code(405) |
paulb@22 | 172 | |
paulb@22 | 173 | # vim: tabstop=4 expandtab shiftwidth=4 |