Le repo des sources pour le site web des JM2L
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

565 lines
23 KiB

  1. # -*- coding: utf8 -*-
  2. from pyramid.view import view_config, view_defaults
  3. from pyramid.response import Response
  4. from pyramid.exceptions import NotFound
  5. from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest
  6. from PIL import Image
  7. import re, os, shutil
  8. from os import path
  9. import mimetypes
  10. import magic
  11. import subprocess
  12. import cStringIO as StringIO
  13. # Database access imports
  14. from .models import User, Place, Tiers, Event, SallePhy
  15. from .blenderthumbnailer import blend_extract_thumb, write_png
  16. from jm2l.const import CurrentYear
  17. MIN_FILE_SIZE = 1 # bytes
  18. MAX_FILE_SIZE = 500000000 # bytes
  19. IMAGE_TYPES = re.compile('image/(gif|p?jpeg|(x-)?png)')
  20. ACCEPTED_MIMES = ['application/pdf',
  21. 'application/vnd.oasis.opendocument.text',
  22. 'application/vnd.oasis.opendocument.text-template',
  23. 'application/vnd.oasis.opendocument.graphics',
  24. 'application/vnd.oasis.opendocument.graphics-template',
  25. 'application/vnd.oasis.opendocument.presentation',
  26. 'application/vnd.oasis.opendocument.presentation-template',
  27. 'application/vnd.oasis.opendocument.spreadsheet',
  28. 'application/vnd.oasis.opendocument.spreadsheet-template',
  29. 'image/svg+xml',
  30. 'application/x-blender'
  31. ]
  32. ACCEPT_FILE_TYPES = IMAGE_TYPES
  33. THUMBNAIL_SIZE = 80
  34. EXPIRATION_TIME = 300 # seconds
  35. IMAGEPATH = [ 'images' ]
  36. DOCPATH = [ 'document' ]
  37. THUMBNAILPATH = [ 'images', 'thumbnails' ]
  38. # change the following to POST if DELETE isn't supported by the webserver
  39. DELETEMETHOD="DELETE"
  40. mimetypes.init()
  41. class MediaPath():
  42. def get_list(self, media_table, linked_id, MediaType=None):
  43. filelist = list()
  44. curpath = self.get_mediapath(media_table, linked_id, None)
  45. if not os.path.isdir(curpath):
  46. return list()
  47. for f in os.listdir(curpath):
  48. if os.path.isdir(os.path.join(curpath,f)):
  49. continue
  50. if f.endswith('.type'):
  51. continue
  52. if f:
  53. filename, ext = os.path.splitext( f )
  54. tmpurl = '/image/%s/%d/%s' % (media_table, linked_id, f.replace(" ","%20"))
  55. if MediaType is None:
  56. filelist.append(tmpurl)
  57. elif MediaType=='Image' and ext.lower() in ['.gif','.jpg','.png','.svg','.jpeg']:
  58. filelist.append(tmpurl)
  59. elif MediaType=='Other' and ext.lower() not in ['.gif','.jpg','.png','.svg','.jpeg']:
  60. filelist.append(tmpurl)
  61. return filelist
  62. def get_thumb(self, media_table, linked_id, MediaType=None):
  63. filelist = list()
  64. curpath = self.get_mediapath(media_table, linked_id, None)
  65. curpath = os.path.join( curpath, 'thumbnails')
  66. if not os.path.isdir(curpath):
  67. return list()
  68. for f in os.listdir(curpath):
  69. filename, ext = os.path.splitext( f )
  70. if os.path.isdir(os.path.join(curpath,f)):
  71. continue
  72. if f.endswith('.type'):
  73. continue
  74. if f:
  75. tmpurl = '/image/%s/%d/thumbnails/%s' % (media_table, linked_id, f.replace(" ","%20"))
  76. if MediaType is None:
  77. filelist.append(tmpurl)
  78. elif MediaType=='Image' and len( os.path.splitext(filename)[1] )==0:
  79. filelist.append(tmpurl)
  80. elif MediaType=='Other' and len( os.path.splitext(filename)[1] ):
  81. filelist.append(tmpurl)
  82. return filelist
  83. def move_mediapath(self, media_table, from_id, to_id):
  84. """
  85. Move target media folder to follow database
  86. :param media_table: type of media
  87. :param from_id: source
  88. :param to_id: destination
  89. :return: Error if any
  90. """
  91. if media_table in ['tiers', 'place', 'salle', 'users']:
  92. src = IMAGEPATH + [ media_table, from_id ]
  93. dst = IMAGEPATH + [ media_table, to_id ]
  94. else:
  95. raise RuntimeError("Sorry, Media '%s' not supported yet for move." % media_table)
  96. src_path = os.path.join('jm2l/upload', *src)
  97. dst_path = os.path.join('jm2l/upload', *dst)
  98. if not os.path.isdir(src_path):
  99. # Nothing to do ...
  100. return False
  101. if os.path.isdir(dst_path):
  102. raise RuntimeError('Destination path already exist')
  103. shutil.move(src_path, dst_path)
  104. return True
  105. def get_mediapath(self, media_table, linked_id, name):
  106. """
  107. :param media_table: type of media
  108. :param linked_id: id of media
  109. :param name: filename
  110. :return: full relative path on server side
  111. """
  112. linked_id = str(linked_id)
  113. if media_table in ['tiers', 'place', 'salle']:
  114. # Retrieve Slug
  115. if media_table=='tiers':
  116. slug = Tiers.by_id(linked_id).slug
  117. if media_table=='place':
  118. slug = Place.by_id(linked_id).slug
  119. if media_table=='salle':
  120. slug = SallePhy.by_id(linked_id).slug
  121. p = IMAGEPATH + [ media_table, slug ]
  122. elif media_table=='presse':
  123. # Use Year in linked_id
  124. p = IMAGEPATH + [ media_table, linked_id ]
  125. elif media_table=='tasks':
  126. # Use Current Year
  127. p = IMAGEPATH + [ str(CurrentYear), media_table, linked_id ]
  128. elif media_table=='poles':
  129. # Use Current Year
  130. p = IMAGEPATH + [ str(CurrentYear), media_table, linked_id ]
  131. elif media_table in ['RIB', 'Justif']:
  132. slug = User.by_id(linked_id).slug
  133. p = IMAGEPATH + ['users', slug , media_table ]
  134. elif media_table in ['users', 'badge']:
  135. user = User.by_id(linked_id)
  136. if not user:
  137. raise HTTPNotFound()
  138. else:
  139. slug = user.slug
  140. p = IMAGEPATH + [media_table, slug ]
  141. elif media_table=='event':
  142. ev = Event.by_id(linked_id)
  143. slug = ev.slug
  144. year = ev.for_year
  145. p = IMAGEPATH + ['event', str(year), slug ]
  146. if name:
  147. p += [ name ]
  148. TargetPath = os.path.join('jm2l/upload', *p)
  149. if not os.path.isdir(os.path.dirname(TargetPath)):
  150. try:
  151. os.makedirs(os.path.dirname(TargetPath))
  152. except OSError, e:
  153. if e.errno != 17:
  154. raise e
  155. return os.path.join('jm2l/upload', *p)
  156. def ExtMimeIcon(self, mime):
  157. if mime=='application/pdf':
  158. return "/img/PDF.png"
  159. def check_blend_file(self, fileobj):
  160. head = fileobj.read(12)
  161. fileobj.seek(0)
  162. if head[:2] == b'\x1f\x8b': # gzip magic
  163. import zlib
  164. head = zlib.decompress(fileobj.read(), 31)[:12]
  165. fileobj.seek(0)
  166. if head.startswith(b'BLENDER'):
  167. return True
  168. def get_mimetype_from_file(self, fileobj):
  169. mimetype = magic.from_buffer(fileobj.read(1024), mime=True)
  170. fileobj.seek(0)
  171. # Check if the binary file is a blender file
  172. if ( mimetype == "application/octet-stream" or mimetype == "application/x-gzip" ) and self.check_blend_file(fileobj):
  173. return "application/x-blender", True
  174. else:
  175. return mimetype, False
  176. def get_mimetype(self, name):
  177. """ This function return the mime-type based on .type file """
  178. try:
  179. with open(self.mediapath(name) + '.type', 'r', 16) as f:
  180. mime = f.read()
  181. return mime
  182. except IOError:
  183. return None
  184. @view_defaults(route_name='media_upload')
  185. class MediaUpload(MediaPath):
  186. def __init__(self, request):
  187. self.request = request
  188. self.media_table = self.request.matchdict.get('media_table')
  189. self.display_only = False
  190. if self.media_table.startswith('_'):
  191. self.display_only = True
  192. self.media_table = self.media_table[1:]
  193. self.linked_id = self.request.matchdict.get('uid')
  194. if not self.linked_id.isdigit():
  195. raise HTTPBadRequest('Wrong Parameter')
  196. request.response.headers['Access-Control-Allow-Origin'] = '*'
  197. request.response.headers['Access-Control-Allow-Methods'] = 'OPTIONS, HEAD, GET, POST, PUT, DELETE'
  198. def mediapath(self, name):
  199. return self.get_mediapath(self.media_table, self.linked_id, name)
  200. def validate(self, result, filecontent):
  201. """ Do some basic check to the uploaded file before to accept it """
  202. # Try to determine mime type from content uploaded
  203. found_mime = magic.from_buffer(filecontent.read(1024), mime=True)
  204. filecontent.seek(0)
  205. # Do a special statement for specific detected mime type
  206. if found_mime in ["application/octet-stream", "application/x-gzip"]:
  207. # Lets see if it's a bender file
  208. if self.check_blend_file(filecontent):
  209. found_mime = "application/x-blender"
  210. # MonKey Patch of content type
  211. result['type'] = found_mime
  212. # Reject mime type that don't match
  213. if found_mime!=result['type']:
  214. result['error'] = 'L\'extension du fichier ne correspond pas à son contenu - '
  215. result['error'] += "( %s vs %s )" % (found_mime, result['type'])
  216. return False
  217. # Accept images and mime types listed
  218. if not found_mime in ACCEPTED_MIMES:
  219. if not (IMAGE_TYPES.match(found_mime)):
  220. result['error'] = 'Ce type fichier n\'est malheureusement pas supporté. '
  221. return False
  222. if result['size'] < MIN_FILE_SIZE:
  223. result['error'] = 'le fichier est trop petit'
  224. elif result['size'] > MAX_FILE_SIZE:
  225. result['error'] = 'le fichier est trop voluminueux'
  226. #elif not ACCEPT_FILE_TYPES.match(file['type']):
  227. # file['error'] = u'les type de fichiers acceptés sont png, jpg et gif'
  228. else:
  229. return True
  230. return False
  231. def get_file_size(self, fileobj):
  232. fileobj.seek(0, 2) # Seek to the end of the file
  233. size = fileobj.tell() # Get the position of EOF
  234. fileobj.seek(0) # Reset the file position to the beginning
  235. return size
  236. def thumbnailurl(self,name):
  237. return self.request.route_url('media_view',name='thumbnails',
  238. media_table=self.media_table,
  239. uid=self.linked_id) + '/' + name
  240. def thumbnailpath(self,name):
  241. origin = self.mediapath(name)
  242. TargetPath = os.path.join( os.path.dirname(origin), 'thumbnails', name)
  243. if not os.path.isdir(os.path.dirname(TargetPath)):
  244. os.makedirs(os.path.dirname(TargetPath))
  245. return TargetPath
  246. def createthumbnail(self, filename):
  247. image = Image.open( self.mediapath(filename) )
  248. image.thumbnail((THUMBNAIL_SIZE, THUMBNAIL_SIZE), Image.ANTIALIAS)
  249. timage = Image.new('RGBA', (THUMBNAIL_SIZE, THUMBNAIL_SIZE), (255, 255, 255, 0))
  250. timage.paste(
  251. image,
  252. ((THUMBNAIL_SIZE - image.size[0]) / 2, (THUMBNAIL_SIZE - image.size[1]) / 2))
  253. TargetFileName = self.thumbnailpath(filename)
  254. timage.save( TargetFileName )
  255. return self.thumbnailurl( os.path.basename(TargetFileName) )
  256. def pdfthumbnail(self, filename):
  257. TargetFileName = self.thumbnailpath(filename)
  258. Command = ["convert","./%s[0]" % self.mediapath(filename),"./%s_.jpg" % TargetFileName]
  259. Result = subprocess.call(Command)
  260. if Result==0:
  261. image = Image.open( TargetFileName+"_.jpg" )
  262. pdf_indicator = Image.open( "jm2l/static/img/PDF_Thumb_Stamp.png" )
  263. image.thumbnail((THUMBNAIL_SIZE, THUMBNAIL_SIZE), Image.ANTIALIAS)
  264. timage = Image.new('RGBA', (THUMBNAIL_SIZE, THUMBNAIL_SIZE), (255, 255, 255, 0))
  265. # Add thumbnail
  266. timage.paste(
  267. image,
  268. ((THUMBNAIL_SIZE - image.size[0]) / 2, (THUMBNAIL_SIZE - image.size[1]) / 2))
  269. # Stamp with PDF file type
  270. timage.paste(
  271. pdf_indicator,
  272. (timage.size[0]-30, timage.size[1]-30),
  273. pdf_indicator,
  274. )
  275. timage.convert('RGB').save( TargetFileName+".jpg", 'JPEG')
  276. os.unlink(TargetFileName+"_.jpg")
  277. return self.thumbnailurl( os.path.basename(TargetFileName+".jpg") )
  278. return self.ExtMimeIcon('application/pdf')
  279. def svgthumbnail(self, filename):
  280. TargetFileName = self.thumbnailpath(filename)
  281. Command = ["convert","./%s[0]" % self.mediapath(filename),"./%s_.jpg" % TargetFileName]
  282. Result = subprocess.call(Command)
  283. if Result==0:
  284. image = Image.open( TargetFileName+"_.jpg" )
  285. pdf_indicator = Image.open( "jm2l/static/img/svg-icon.png" )
  286. image.thumbnail((THUMBNAIL_SIZE, THUMBNAIL_SIZE), Image.ANTIALIAS)
  287. timage = Image.new('RGBA', (THUMBNAIL_SIZE, THUMBNAIL_SIZE), (255, 255, 255, 0))
  288. # Add thumbnail
  289. timage.paste(
  290. image,
  291. ((THUMBNAIL_SIZE - image.size[0]) / 2, (THUMBNAIL_SIZE - image.size[1]) / 2))
  292. # Stamp with PDF file type
  293. timage.paste(
  294. pdf_indicator,
  295. (timage.size[0]-30, timage.size[1]-30),
  296. pdf_indicator,
  297. )
  298. timage.convert('RGB').save( TargetFileName+".jpg", 'JPEG')
  299. os.unlink(TargetFileName+"_.jpg")
  300. return self.thumbnailurl( os.path.basename(TargetFileName+".jpg") )
  301. return self.ExtMimeIcon('image/svg+xml')
  302. def docthumbnail(self, filename):
  303. TargetFileName = self.thumbnailpath(filename)
  304. # let's take the thumbnail generated inside the document
  305. Command = ["unzip", "-p", self.mediapath(filename), "Thumbnails/thumbnail.png"]
  306. ThumbBytes = subprocess.check_output(Command)
  307. image = Image.open( StringIO.StringIO(ThumbBytes) )
  308. image.thumbnail((THUMBNAIL_SIZE, THUMBNAIL_SIZE), Image.ANTIALIAS)
  309. # Use the correct stamp
  310. f, ext = os.path.splitext( filename )
  311. istamp = [ ('Writer','odt'),
  312. ('Impress','odp'),
  313. ('Calc','ods'),
  314. ('Draw','odg')]
  315. stampfilename = filter(lambda (x,y): ext.endswith(y), istamp)
  316. stamp = Image.open( "jm2l/static/img/%s-icon.png" % stampfilename[0][0])
  317. timage = Image.new('RGBA', (THUMBNAIL_SIZE, THUMBNAIL_SIZE), (255, 255, 255, 0))
  318. # Add thumbnail
  319. timage.paste(
  320. image,
  321. ((THUMBNAIL_SIZE - image.size[0]) / 2, (THUMBNAIL_SIZE - image.size[1]) / 2))
  322. # Stamp with PDF file type
  323. timage.paste(
  324. stamp,
  325. (timage.size[0]-30, timage.size[1]-30),
  326. stamp,
  327. )
  328. timage.convert('RGB').save( TargetFileName+".jpg", 'JPEG')
  329. return self.thumbnailurl( os.path.basename(TargetFileName+".jpg") )
  330. def blendthumbnail(self, filename):
  331. blendfile = self.mediapath(filename)
  332. # Extract Thumb
  333. if 0:
  334. head = fileobj.read(12)
  335. fileobj.seek(0)
  336. if head[:2] == b'\x1f\x8b': # gzip magic
  337. import zlib
  338. head = zlib.decompress(fileobj.read(), 31)[:12]
  339. fileobj.seek(0)
  340. buf, width, height = blend_extract_thumb(blendfile)
  341. if buf:
  342. png = write_png(buf, width, height)
  343. TargetFileName = self.thumbnailpath(filename)
  344. image = Image.open(StringIO.StringIO(png))
  345. blender_indicator = Image.open( "jm2l/static/img/Blender_Thumb_Stamp.png" )
  346. image.thumbnail((THUMBNAIL_SIZE, THUMBNAIL_SIZE), Image.ANTIALIAS)
  347. timage = Image.new('RGBA', (THUMBNAIL_SIZE, THUMBNAIL_SIZE), (255, 255, 255, 0))
  348. # Add thumbnail
  349. timage.paste(
  350. image,
  351. ((THUMBNAIL_SIZE - image.size[0]) / 2, (THUMBNAIL_SIZE - image.size[1]) / 2))
  352. # Stamp with Blender file type
  353. timage.paste(
  354. blender_indicator,
  355. (timage.size[0]-30, timage.size[1]-30),
  356. blender_indicator,
  357. )
  358. timage.save( TargetFileName+".png")
  359. return self.thumbnailurl( os.path.basename(TargetFileName+".png") )
  360. return self.ExtMimeIcon('application/x-blender')
  361. def fileinfo(self,name):
  362. filename = self.mediapath(name)
  363. f, ext = os.path.splitext(name)
  364. if ext!='.type' and os.path.isfile(filename):
  365. info = {}
  366. info['name'] = name
  367. info['size'] = os.path.getsize(filename)
  368. info['url'] = self.request.route_url('media_view',
  369. name=name,
  370. media_table=self.media_table,
  371. uid=self.linked_id)
  372. mime = self.get_mimetype(name)
  373. if IMAGE_TYPES.match(mime):
  374. info['thumbnailUrl'] = self.thumbnailurl(name)
  375. elif mime in ACCEPTED_MIMES:
  376. thumb = self.thumbnailpath("%s%s" % (f, ext))
  377. thumbext = ".jpg"
  378. if mime == "application/x-blender":
  379. thumbext = ".png"
  380. if os.path.exists( thumb + thumbext ):
  381. info['thumbnailUrl'] = self.thumbnailurl(name)+thumbext
  382. else:
  383. info['thumbnailUrl'] = self.ExtMimeIcon(mime)
  384. else:
  385. info['thumbnailUrl'] = self.ExtMimeIcon(mime)
  386. if not self.display_only:
  387. info['deleteType'] = DELETEMETHOD
  388. info['deleteUrl'] = self.request.route_url('media_upload',
  389. sep='',
  390. name='',
  391. media_table=self.media_table,
  392. uid=self.linked_id) + '/' + name
  393. if DELETEMETHOD != 'DELETE':
  394. info['deleteUrl'] += '&_method=DELETE'
  395. return info
  396. else:
  397. return None
  398. @view_config(request_method='OPTIONS')
  399. def options(self):
  400. return Response(body='')
  401. @view_config(request_method='HEAD')
  402. def options(self):
  403. return Response(body='')
  404. @view_config(request_method='GET', renderer="json")
  405. def get(self):
  406. p = self.request.matchdict.get('name')
  407. if p:
  408. return self.fileinfo(p)
  409. else:
  410. filelist = []
  411. content = self.mediapath('')
  412. if content and path.exists(content):
  413. for f in os.listdir(content):
  414. n = self.fileinfo(f)
  415. if n:
  416. filelist.append(n)
  417. return { "files":filelist }
  418. @view_config(request_method='DELETE', xhr=True, accept="application/json", renderer='json')
  419. def delete(self):
  420. filename = self.request.matchdict.get('name')
  421. try:
  422. os.remove(self.mediapath(filename) + '.type')
  423. except IOError:
  424. pass
  425. except OSError:
  426. pass
  427. try:
  428. os.remove(self.thumbnailpath(filename))
  429. except IOError:
  430. pass
  431. except OSError:
  432. pass
  433. try:
  434. os.remove(self.thumbnailpath(filename+".jpg"))
  435. except IOError:
  436. pass
  437. except OSError:
  438. pass
  439. try:
  440. os.remove(self.mediapath(filename))
  441. except IOError:
  442. return False
  443. return True
  444. @view_config(request_method='POST', xhr=True, accept="application/json", renderer='json')
  445. def post(self):
  446. if self.request.matchdict.get('_method') == "DELETE":
  447. return self.delete()
  448. results = []
  449. for name, fieldStorage in self.request.POST.items():
  450. if isinstance(fieldStorage,unicode):
  451. continue
  452. result = {}
  453. result['name'] = os.path.basename(fieldStorage.filename)
  454. result['type'] = fieldStorage.type
  455. result['size'] = self.get_file_size(fieldStorage.file)
  456. if self.validate(result, fieldStorage.file):
  457. # Keep mime-type in .type file
  458. with open( self.mediapath( result['name'] ) + '.type', 'w') as f:
  459. f.write(result['type'])
  460. # Store uploaded file
  461. fieldStorage.file.seek(0)
  462. with open( self.mediapath(result['name'] ), 'wb') as f:
  463. shutil.copyfileobj( fieldStorage.file , f)
  464. if re.match(IMAGE_TYPES, result['type']):
  465. result['thumbnailUrl'] = self.createthumbnail(result['name'])
  466. elif result['type']=='application/pdf':
  467. result['thumbnailUrl'] = self.pdfthumbnail(result['name'])
  468. elif result['type']=='image/svg+xml':
  469. result['thumbnailUrl'] = self.svgthumbnail(result['name'])
  470. elif result['type'].startswith('application/vnd'):
  471. result['thumbnailUrl'] = self.docthumbnail(result['name'])
  472. elif result['type']=='application/x-blender':
  473. result['thumbnailUrl'] = self.blendthumbnail(result['name'])
  474. else:
  475. result['thumbnailUrl'] = self.ExtMimeIcon(result['type'])
  476. result['deleteType'] = DELETEMETHOD
  477. result['deleteUrl'] = self.request.route_url('media_upload',
  478. sep='',
  479. name='',
  480. media_table=self.media_table,
  481. uid=self.linked_id) + '/' + result['name']
  482. result['url'] = self.request.route_url('media_view',
  483. media_table=self.media_table,
  484. uid=self.linked_id,
  485. name=result['name'])
  486. if DELETEMETHOD != 'DELETE':
  487. result['deleteUrl'] += '&_method=DELETE'
  488. results.append(result)
  489. return {"files":results}
  490. @view_defaults(route_name='media_view')
  491. class MediaView(MediaPath):
  492. def __init__(self,request):
  493. self.request = request
  494. self.media_table = self.request.matchdict.get('media_table')
  495. self.linked_id = self.request.matchdict.get('uid')
  496. def mediapath(self,name):
  497. return self.get_mediapath(self.media_table, self.linked_id, name)
  498. @view_config(request_method='GET', http_cache = (EXPIRATION_TIME, {'public':True}))
  499. def get(self):
  500. name = self.request.matchdict.get('name')
  501. self.request.response.content_type = self.get_mimetype(name)
  502. try:
  503. self.request.response.body_file = open( self.mediapath(name), 'rb', 10000)
  504. except IOError:
  505. raise NotFound
  506. return self.request.response
  507. ##return Response(app_iter=ImgHandle, content_type = 'image/png')