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