Package qm :: Module attachment
[hide private]
[frames] | no frames]

Source Code for Module qm.attachment

  1  ######################################################################## 
  2  # 
  3  # File:   attachment.py 
  4  # Author: Alex Samuel 
  5  # Date:   2001-03-21 
  6  # 
  7  # Contents: 
  8  #   Generic code for handling arbitrary file attachments. 
  9  # 
 10  # Copyright (c) 2001, 2002, 2003 by CodeSourcery, LLC.  All rights reserved.  
 11  # 
 12  # For license terms see the file COPYING. 
 13  # 
 14  ######################################################################## 
 15   
 16  """Code for handling arbitrary file attachments. 
 17   
 18  'Attachment' is a base class for classes that represent arbitrary 
 19  attachments.  Each 'Attachment' object has these four attributes: 
 20   
 21    'mime_type' -- The MIME type of the attachment contents.  This 
 22    information enables user interfaces to handle attachment data in a 
 23    sensible fasion. 
 24   
 25    'description' -- The user's description of the attachment contents. 
 26   
 27    'file_name' -- A file name associated with the description.  This is 
 28    usually the name of the file from which the attachment was originally 
 29    uploaded or inserted. 
 30   
 31    'location' -- A string containing the external location of the 
 32    attachment data.  The semantics of this string are defined by 
 33    implementations of 'AttachmentStore', which use it to locate the 
 34    attachment's data. 
 35   
 36  A special 'TemporaryAttachmentStore', with a different interface, is 
 37  used to store attachment data temporarily, at most for the life of the 
 38  program.  The 'temporary_store' global instance should be used."""  
 39   
 40  ######################################################################## 
 41  # imports 
 42  ######################################################################## 
 43   
 44  import common 
 45  import mimetypes 
 46  import os 
 47  import xmlutil 
 48  import temporary_directory 
 49   
 50  ######################################################################## 
 51  # classes 
 52  ######################################################################## 
 53   
54 -class Attachment:
55 """An arbitrary file attachment. 56 57 Conceptually, an attachment is composed of these parts: 58 59 1. A MIME type, as a string. 60 61 2. A description, as a structured text string. 62 63 3. A file name, corresponding to the original name of the file from 64 which the attachment was uploaded, or the name of the file to 65 use when the attachment is presented to the user in a file 66 system. 67 68 4. A block of arbitrary data. 69 70 For efficiency reasons, the attachment data is not stored in the 71 attachment. Instead, a *location* is stored, which is a key into 72 the associated 'AttachmentStore' object.""" 73
74 - def __init__(self, 75 mime_type, 76 description, 77 file_name, 78 location, 79 store):
80 """Create a new attachment. 81 82 'mime_type' -- The MIME type. If 'None' or an empty string, the 83 function attempts to guess the MIME type from other information. 84 85 'description' -- A description of the attachment contents. 86 87 'file_name' -- The user-visible file name to associate the 88 attachment. 89 90 'location' -- The location in an attachment store at which to 91 find the attachment data. 92 93 'store' -- The attachment store in which the data is stored.""" 94 95 # If no MIME type is specified, try to guess it from the file 96 # name. 97 if mime_type == "" or mime_type is None: 98 mime_type = mimetypes.guess_type(file_name)[0] 99 if mime_type is None: 100 # Couldn't guess from the file name. Use a safe 101 # default. 102 mime_type = "application/octet-stream" 103 self.__mime_type = mime_type 104 # Store other attributes. 105 self.__description = description 106 self.__file_name = file_name 107 self.__location = location 108 self.__store = store
109 110
111 - def GetMimeType(self):
112 """Return the attachment's MIME type.""" 113 114 return self.__mime_type
115 116
117 - def GetDescription(self):
118 """Return the attachment's description.""" 119 120 return self.__description
121 122
123 - def GetFileName(self):
124 """Return the attachment's file name.""" 125 126 return self.__file_name
127 128
129 - def GetLocation(self):
130 """Return the attachment's location in an attachment store.""" 131 132 return self.__location
133 134
135 - def GetData(self):
136 """Get attachment data. 137 138 returns -- The attachment data.""" 139 140 return self.GetStore().GetData(self.GetLocation())
141 142
143 - def GetDataFile(self):
144 """Return the path to a file containing attachment data. 145 146 returns -- A file system path. The file should be considered 147 read-only, and should not be modified in any way.""" 148 149 return self.GetStore().GetDataFile(self.GetLocation())
150 151
152 - def GetStore(self):
153 """Return the store in which this attachment is located. 154 155 returns -- The 'AttachmentStore' that contains this attachment.""" 156 157 return self.__store
158 159
160 - def Move(self, store, location):
161 """Move the 'Attachment' to a new location. 162 163 'store' -- The 'AttachmentStore' that will contain the 164 attachment. 165 166 'location' -- The location of the attachment within its current 167 store.""" 168 169 # Store this attachment in its new location. That must be done 170 # before removing it from its current location as that step will 171 # destroy the data contained in the attachment. 172 store.Store(self, location) 173 # Now, remove the attachment from its current location. 174 self.__store.Remove(self.__location) 175 # Finally, update the information associated with the attachment. 176 self.__store = store 177 self.__location = location
178 179
180 - def __str__(self):
181 return '<Attachment "%s" (%s)>' \ 182 % (self.GetDescription(), self.GetMimeType())
183 184
185 - def __cmp__(self, other):
186 return other is None \ 187 or self.GetDescription() != other.GetDescription() \ 188 or self.GetMimeType() != other.GetMimeType() \ 189 or self.GetFileName() != other.GetFileName() \ 190 or self.GetLocation() != other.GetLocation() \ 191 or self.GetStore() != other.GetStore()
192 193 194
195 -class AttachmentStore(object):
196 """Interface for classes which store attachment data. 197 198 An attachment store stores the raw data for an attachment. The 199 store is not responsible for storing auxiliary information, 200 including the attachment's description, file name, or MIME type. 201 202 Users of an 'AttachmentStore' reference attachment data by a 203 *location*, which is stored with the attachment. 204 205 Please note that the 'AttachmentStore' interface provides methods 206 for retrieving attachment data only; not for storing it. The 207 interface for storing may be defined in any way by implementations.""" 208
209 - def GetData(self, location):
210 """Return the data for an attachment. 211 212 returns -- A string containing the attachment data.""" 213 214 raise NotImplementedError
215 216
217 - def GetDataFile(self, location):
218 """Return the path to a file containing the data for 219 'attachment'. 220 221 returns -- A file system path. 222 223 The file is read-only, and may be a temporary file. The caller 224 should not modify the file in any way.""" 225 226 raise NotImplementedError
227 228
229 - def GetSize(self, location):
230 """Return the size of the data for an attachment. 231 232 returns -- The length of the attachment data, in bytes. 233 234 This method may be overridden by derived classes.""" 235 236 return len(self.GetData(location))
237 238
239 - def HandleDownloadRequest(self, request):
240 """Handle a web request to download attachment data. 241 242 'request' -- A 'WebRequest' object. The location of the 243 attachment data is stored in the 'location' property, and the 244 MIME type in the 'mime_type' property. 245 246 returns -- A pair '(mime_type, data)' where 'mime_type' is the 247 MIME type stored in the request and 'data' is the contents of 248 the attachment.""" 249 250 location = request["location"] 251 mime_type = request["mime_type"] 252 data = self.GetData(location) 253 return (mime_type, data)
254 255
256 - def Store(self, attachment, location):
257 """Add an attachment to the store. 258 259 'attachment' -- The 'Attachment' to store. 260 261 'location' -- The location in which to store the 'attachment'.""" 262 263 raise NotImplementedError
264 265 266
267 -class FileAttachmentStore(AttachmentStore):
268 """An attachment store based on the file system. 269 270 The locations are the names of files in the file system.""" 271
272 - def __init__(self, root = None):
273 """Construct a new 'FileAttachmentStore' 274 275 'root' -- If not 'None', the root directory for the store. All 276 locations are relative to this directory. If 'None', all 277 locations are relative to the current directory.""" 278 279 super(AttachmentStore, self).__init__() 280 self.__root = root
281 282
283 - def GetData(self, location):
284 285 # Open the file. 286 f = open(self.GetDataFile(location)) 287 # Read the contents. 288 s = f.read() 289 # Close the file. 290 f.close() 291 292 return s
293 294
295 - def GetDataFile(self, location):
296 297 if self.__root is not None: 298 # It might seem sensible to assert that the location be a 299 # relative path, but that would break backwards 300 # compatibility with older versions fo QMTest. In those 301 # older versions, the XMLDatabase sometimes used an absolute 302 # path for attachment locations. 303 return os.path.join(self.__root, location) 304 else: 305 return location
306 307
308 - def GetSize(self, location):
309 310 return os.stat(self.GetDataFile(location))[6]
311 312
313 - def Store(self, attachment, location):
314 315 # Create the file. 316 file = open(self.GetDataFile(location), "w") 317 # Write the data. 318 file.write(attachment.GetData()) 319 # Close the file. 320 file.close()
321 322
323 - def Remove(self, location):
324 """Remove an attachment. 325 326 'location' -- The location whose data should be removed.""" 327 328 os.remove(self.GetDataFile(location))
329 330 331
332 -class TemporaryAttachmentStore(FileAttachmentStore):
333 """Temporary storage for attachment data. 334 335 A 'TemporaryAttachmentStore' stores attachment data in a temporary 336 location, for up to the lifetime of the running program. When the 337 program ends, all temporarily stored attachment data is deleted. 338 339 A data object in the temporary store is identified by its location. 340 Locations should be generated by 'make_temporary_location'.""" 341
342 - def __init__(self):
343 """Construct a temporary attachment store. 344 345 The store is initially empty.""" 346 347 # Construct a temporary directory in which to store attachment 348 # data. 349 self.__tmpdir = temporary_directory.TemporaryDirectory() 350 # Initialize the base class. 351 path = self.__tmpdir.GetPath() 352 super(TemporaryAttachmentStore, self).__init__(path)
353 354
355 - def HandleUploadRequest(self, request):
356 """Handle a web request to upload attachment data. 357 358 Store the attachment data contained in the request as a 359 temporary attachment. It is assumed that the request is being 360 submitted from a popup upload browser window, so the returned 361 HTML page instructs the window to close itself. 362 363 'request' -- A 'WebRequest' object. 364 365 returns -- HTML text of a page that instructs the browser window 366 to close.""" 367 368 location = request["location"] 369 # Create the file. 370 file = open(self.GetDataFile(location), "w") 371 # Write the data. 372 file.write(request["file_data"]) 373 # Close the file. 374 file.close() 375 # Return a page that closes the popup window from which the 376 # attachment was submitted. 377 return ''' 378 <html><body> 379 <script type="text/javascript" language="JavaScript"> 380 window.close(); 381 </script> 382 </body></html> 383 '''
384 385 ######################################################################## 386 # functions 387 ######################################################################## 388 389 _temporary_location_prefix = "_temporary" 390 391
392 -def make_temporary_location():
393 """Return a unique location for temporary attachment data.""" 394 395 return _temporary_location_prefix + common.make_unique_tag()
396 397
398 -def make_dom_node(attachment, document):
399 """Create a DOM element node for this attachment. 400 401 'document' -- A DOM document node in which to create the 402 element. 403 404 returns -- A DOM element node.""" 405 406 # Create an attachment element. 407 node = document.createElement("attachment") 408 # Is it a null attachment? 409 if attachment is None: 410 # Then that's it. 411 return node 412 413 mime_type = attachment.GetMimeType() 414 415 # Create and add the description node. 416 child = xmlutil.create_dom_text_element( 417 document, "description", attachment.GetDescription()) 418 node.appendChild(child) 419 # Create and add the MIME type node. 420 child = xmlutil.create_dom_text_element( 421 document, "mime-type", mime_type) 422 node.appendChild(child) 423 # Create and add the file name node. 424 child = xmlutil.create_dom_text_element( 425 document, "filename", attachment.GetFileName()) 426 node.appendChild(child) 427 # Create a location element, to include attachment data by 428 # reference. 429 location = attachment.GetLocation() 430 child = xmlutil.create_dom_text_element(document, "location", location) 431 432 node.appendChild(child) 433 return node
434 435
436 -def from_dom_node(node, store):
437 """Construct an attachment object from a DOM element node. 438 439 'node' -- A DOM attachment element node. 440 441 'store' -- The associated attachment store. 442 443 returns -- An attachment instance. The type is determined by 444 'attachment_class'. 445 446 If the attachment object requires additional context information to 447 interpret the location (if it's specified in the attachment 448 element), the caller must provide it directly to the object.""" 449 450 if len(node.childNodes) == 0: 451 # It's an empty element, signifying a null attachment. 452 return None 453 454 # Extract the fixed fields; use a default value for each that is not 455 # present. 456 description = xmlutil.get_child_text(node, "description", "") 457 mime_type = xmlutil.get_child_text( 458 node, "mime-type", "application/octet-stream") 459 file_name = xmlutil.get_child_text(node, "filename", "") 460 location = xmlutil.get_child_text(node, "location") 461 # Construct the resulting attachment. 462 return Attachment(mime_type, description, file_name, location, store)
463 464 ######################################################################## 465 # Local Variables: 466 # mode: python 467 # indent-tabs-mode: nil 468 # fill-column: 72 469 # End: 470