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.
 
 
 
 
 

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