1   
  2   
  3   
  4   
  5   
  6  from __future__ import with_statement 
  7  import base64 
  8  import copy 
  9  from hashlib import md5 
 10  import logging 
 11  import mimetypes 
 12  import os 
 13  import os.path 
 14  import re 
 15   
 16  from .. import client 
 17  from ..exceptions import ResourceNotFound, DesignerError, \ 
 18  BulkSaveError 
 19  from .macros import package_shows, package_views 
 20  from .. import utils 
 21   
 22  if os.name == 'nt': 
 24          return name.replace("\\", "/") 
  25   
 27          return name.replace("/", "\\") 
  28  else: 
 31   
 34   
 35  logger = logging.getLogger(__name__) 
 36   
 38   
 39 -    def __init__(self, path, create=False, docid=None, is_ddoc=True): 
  40          self.docdir = path 
 41          self.ignores = [] 
 42          self.is_ddoc = is_ddoc 
 43   
 44          ignorefile = os.path.join(path, '.couchappignore') 
 45          if os.path.exists(ignorefile): 
 46               
 47               
 48              self.ignores = utils.json.load(open(ignorefile, 'r')) 
 49          if not docid: 
 50              docid = self.get_id() 
 51          self.docid = docid 
 52          self._doc = {'_id': self.docid} 
 53          if create: 
 54              self.create() 
  55   
 57          """ 
 58          if there is an _id file, docid is extracted from it, 
 59          else we take the current folder name. 
 60          """ 
 61          idfile = os.path.join(self.docdir, '_id') 
 62          if os.path.exists(idfile): 
 63              docid = utils.read_file(idfile).split("\n")[0].strip() 
 64              if docid: return docid 
 65          if self.is_ddoc: 
 66              return "_design/%s" % os.path.split(self.docdir)[1] 
 67          else: 
 68              return os.path.split(self.docdir)[1] 
  69   
 71          return "<%s (%s/%s)>" % (self.__class__.__name__, self.docdir, self.docid) 
  72   
 75   
 77          if not os.path.isdir(self.docdir): 
 78              logger.error("%s directory doesn't exist." % self.docdir) 
 79   
 80          rcfile = os.path.join(self.docdir, '.couchapprc') 
 81          if not os.path.isfile(rcfile): 
 82              utils.write_json(rcfile, {}) 
 83          else: 
 84              logger.warning("CouchApp already initialized in %s." % self.docdir) 
  85   
 86 -    def push(self, dbs, atomic=True, force=False): 
  87          """Push a doc to a list of database `dburls`. If noatomic is true 
 88          each attachments will be sent one by one.""" 
 89          for db in dbs: 
 90              if atomic: 
 91                  doc = self.doc(db, force=force) 
 92                  db.save_doc(doc, force_update=True) 
 93              else: 
 94                  doc = self.doc(db, with_attachments=False, force=force) 
 95                  db.save_doc(doc, force_update=True) 
 96   
 97                  attachments = doc.get('_attachments') or {} 
 98   
 99                  for name, filepath in self.attachments(): 
100                      if name not in attachments: 
101                          logger.debug("attach %s " % name) 
102                          db.put_attachment(doc, open(filepath, "r"), 
103                                              name=name) 
104   
105              logger.debug("%s/%s had been pushed from %s" % (db.uri, 
106                  self.docid, self.docdir)) 
 107   
108   
110          att = {} 
111          with open(filepath, "rb") as f: 
112              re_sp = re.compile('\s') 
113              att = { 
114                      "data": re_sp.sub('',base64.b64encode(f.read())), 
115                      "content_type": ';'.join(filter(None, 
116                                              mimetypes.guess_type(name))) 
117              } 
118   
119          return att 
 120   
121 -    def doc(self, db=None, with_attachments=True, force=False): 
 122          """ Function to reetrieve document object from 
123          document directory. If `with_attachments` is True 
124          attachments will be included and encoded""" 
125   
126          manifest = [] 
127          objects = {} 
128          signatures = {} 
129          attachments = {} 
130   
131          self._doc = {'_id': self.docid} 
132   
133           
134          self._doc.update(self.dir_to_fields(self.docdir, manifest=manifest)) 
135   
136          if not 'couchapp' in self._doc: 
137              self._doc['couchapp'] = {} 
138   
139          self.olddoc = {} 
140          if db is not None: 
141              try: 
142                  self.olddoc = db.open_doc(self._doc['_id']) 
143                  attachments = self.olddoc.get('_attachments') or {} 
144                  self._doc.update({'_rev': self.olddoc['_rev']}) 
145              except ResourceNotFound: 
146                  self.olddoc = {} 
147   
148          if 'couchapp' in self.olddoc: 
149              old_signatures = self.olddoc['couchapp'].get('signatures', 
150                                                          {}) 
151          else: 
152              old_signatures = {} 
153   
154          for name, filepath in self.attachments(): 
155              signatures[name] = utils.sign_file(filepath) 
156              if with_attachments and not old_signatures: 
157                  logger.debug("attach %s " % name) 
158                  attachments[name] = self.attachment_stub(name, filepath) 
159   
160          if old_signatures: 
161              for name, signature in old_signatures.items(): 
162                  cursign = signatures.get(name) 
163                  if not cursign: 
164                      logger.debug("detach %s " % name) 
165                      del attachments[name] 
166                  elif cursign != signature: 
167                      logger.debug("detach %s " % name) 
168                      del attachments[name] 
169                  else: 
170                      continue 
171   
172              if with_attachments: 
173                  for name, filepath in self.attachments(): 
174                      if old_signatures.get(name) != signatures.get(name) or force: 
175                          logger.debug("attach %s " % name) 
176                          attachments[name] = self.attachment_stub(name, filepath) 
177   
178          self._doc['_attachments'] = attachments 
179   
180          self._doc['couchapp'].update({ 
181              'manifest': manifest, 
182              'objects': objects, 
183              'signatures': signatures 
184          }) 
185   
186   
187          if self.docid.startswith('_design/'):   
188              for funs in ['shows', 'lists', 'updates', 'filters', 
189                      'spatial']: 
190                  if funs in self._doc: 
191                      package_shows(self._doc, self._doc[funs], self.docdir, 
192                              objects) 
193   
194              if 'validate_doc_update' in self._doc: 
195                  tmp_dict = dict(validate_doc_update=self._doc[ 
196                                                      "validate_doc_update"]) 
197                  package_shows( self._doc, tmp_dict, self.docdir, 
198                      objects) 
199                  self._doc.update(tmp_dict) 
200   
201              if 'views' in  self._doc: 
202                   
203                   
204                   
205                  views = {} 
206                  dmanifest = {} 
207                  for i, fname in enumerate(manifest): 
208                      if fname.startswith("views/") and fname != "views/": 
209                          name, ext = os.path.splitext(fname) 
210                          if name.endswith('/'): 
211                              name = name[:-1] 
212                          dmanifest[name] = i 
213   
214                  for vname, value in self._doc['views'].iteritems(): 
215                      if value and isinstance(value, dict): 
216                          views[vname] = value 
217                      else: 
218                          del manifest[dmanifest["views/%s" % vname]] 
219                  self._doc['views'] = views 
220                  package_views(self._doc,self._doc["views"], self.docdir, 
221                          objects) 
222   
223              if "fulltext" in self._doc: 
224                  package_views(self._doc,self._doc["fulltext"], self.docdir, 
225                          objects) 
226   
227   
228          return self._doc 
 229   
231          for i in self.ignores: 
232              match = re.match(i, item) 
233              if match: 
234                  logger.debug("ignoring %s" % item) 
235                  return True 
236          return False 
 237   
238 -    def dir_to_fields(self, current_dir='', depth=0, 
239                  manifest=[]): 
 240          """ process a directory and get all members """ 
241   
242          fields={} 
243          if not current_dir: 
244              current_dir = self.docdir 
245          for name in os.listdir(current_dir): 
246              current_path = os.path.join(current_dir, name) 
247              rel_path = _replace_backslash(utils.relpath(current_path, self.docdir)) 
248              if name.startswith("."): 
249                  continue 
250              elif self.check_ignore(name): 
251                  continue 
252              elif depth == 0 and name.startswith('_'): 
253                   
254                  continue 
255              elif name == '_attachments': 
256                  continue 
257              elif depth == 0 and (name == 'couchapp' or name == 'couchapp.json'): 
258                   
259                  if name == "couchapp": 
260                      manifest.append('%s/' % rel_path) 
261                      content = self.dir_to_fields(current_path, 
262                          depth=depth+1, manifest=manifest) 
263                  else: 
264                      manifest.append(rel_path) 
265                      content = utils.read_json(current_path) 
266                      if not isinstance(content, dict): 
267                          content = { "meta": content } 
268                  if 'signatures' in content: 
269                      del content['signatures'] 
270   
271                  if 'manifest' in content: 
272                      del content['manifest'] 
273   
274                  if 'objects' in content: 
275                      del content['objects'] 
276   
277                  if 'length' in content: 
278                      del content['length'] 
279   
280                  if 'couchapp' in fields: 
281                      fields['couchapp'].update(content) 
282                  else: 
283                      fields['couchapp'] = content 
284              elif os.path.isdir(current_path): 
285                  manifest.append('%s/' % rel_path) 
286                  fields[name] = self.dir_to_fields(current_path, 
287                          depth=depth+1, manifest=manifest) 
288              else: 
289                  logger.debug("push %s" % rel_path) 
290   
291                  content = '' 
292                  if name.endswith('.json'): 
293                      try: 
294                          content = utils.read_json(current_path) 
295                      except ValueError: 
296                          logger.error("Json invalid in %s" % current_path) 
297                  else: 
298                      try: 
299                          content = utils.read_file(current_path).strip() 
300                      except UnicodeDecodeError: 
301                          logger.warning("%s isn't encoded in utf8" % current_path) 
302                          content = utils.read_file(current_path, utf8=False) 
303                          try: 
304                              content.encode('utf-8') 
305                          except UnicodeError: 
306                              logger.warning( 
307                              "plan B didn't work, %s is a binary" % current_path) 
308                              logger.warning("use plan C: encode to base64") 
309                              content = "base64-encoded;%s" % base64.b64encode( 
310                                                                          content) 
311   
312   
313                   
314                  name, ext = os.path.splitext(name) 
315                  if name in fields: 
316                      logger.warning( 
317          "%(name)s is already in properties. Can't add (%(fqn)s)" % { 
318                              "name": name, "fqn": rel_path }) 
319                  else: 
320                      manifest.append(rel_path) 
321                      fields[name] = content 
322          return fields 
 323   
325          """ the function processing directory to yeld 
326          attachments. """ 
327          if os.path.isdir(path): 
328              for root, dirs, files in os.walk(path): 
329                  for dirname in dirs: 
330                      if dirname.startswith('.'): 
331                          dirs.remove(dirname) 
332                      elif self.check_ignore(dirname): 
333                          dirs.remove(dirname) 
334                  if files: 
335                      for filename in files: 
336                          if filename.startswith('.'): 
337                              continue 
338                          elif self.check_ignore(filename): 
339                              continue 
340                          else: 
341                              filepath = os.path.join(root, filename) 
342                              name = utils.relpath(filepath, path) 
343                              if vendor is not None: 
344                                  name = os.path.join('vendor', vendor, name) 
345                              name = _replace_backslash(name) 
346                              yield (name, filepath) 
 347   
349          """ This function yield a tuple (name, filepath) corresponding 
350          to each attachment (vendor included) in the couchapp. `name` 
351          is the name of attachment in `_attachments` member and `filepath` 
352          the path to the attachment on the disk. 
353   
354          attachments are processed later to allow us to send attachments inline 
355          or one by one. 
356          """ 
357           
358          attachdir = os.path.join(self.docdir, "_attachments") 
359          for attachment in self._process_attachments(attachdir): 
360              yield attachment 
361          vendordir = os.path.join(self.docdir, 'vendor') 
362          if not os.path.isdir(vendordir): 
363              logger.debug("%s don't exist" % vendordir) 
364              return 
365   
366          for name in os.listdir(vendordir): 
367              current_path = os.path.join(vendordir, name) 
368              if os.path.isdir(current_path): 
369                  attachdir = os.path.join(current_path, '_attachments') 
370                  if os.path.isdir(attachdir): 
371                      for attachment in self._process_attachments(attachdir, 
372                                                          vendor=name): 
373                          yield attachment 
 374   
375 -    def index(self, dburl, index): 
 376          if index is not None: 
377              return "%s/%s/%s" % (dburl, self.docid, index) 
378          elif os.path.isfile(os.path.join(self.docdir, "_attachments", 
379                      'index.html')): 
380              return "%s/%s/index.html" % (dburl, self.docid) 
381          return False 
  382   
383 -def document(path, create=False, docid=None, is_ddoc=True): 
 384      """ simple function to retrive a doc object from filesystem """ 
385      return FSDoc(path, create=create, docid=docid, is_ddoc=is_ddoc) 
 386   
387 -def push(path, dbs, atomic=True, force=False, docid=None): 
 388      """ push a document from the fs to one or more dbs. Identic to 
389      couchapp push command """ 
390      if not isinstance(dbs, (list, tuple)): 
391          dbs = [dbs] 
392   
393      doc = document(path, create=False, docid=docid) 
394      doc.push(dbs, atomic=atomic, force=force) 
395      docspath = os.path.join(path, '_docs') 
396      if os.path.exists(docspath): 
397          pushdocs(docspath, dbs, atomic=atomic) 
 398   
399 -def pushapps(path, dbs, atomic=True, export=False, couchapprc=False): 
 400      """ push all couchapps in one folder like couchapp pushapps command 
401      line """ 
402      if not isinstance(dbs, (list, tuple)): 
403          dbs = [dbs] 
404   
405      apps = [] 
406      for d in os.listdir(path): 
407          appdir = os.path.join(path, d) 
408          if os.path.isdir(appdir): 
409              if couchapprc and not os.path.isfile(os.path.join(appdir, 
410                  '.couchapprc')): 
411                  continue 
412              doc = document(appdir) 
413              if not atomic: 
414                  doc.push(dbs, atomic=False) 
415              else: 
416                  apps.append(doc) 
417      if apps: 
418          if export: 
419              docs= [doc.doc() for doc in apps] 
420              jsonobj = {'docs': docs} 
421              return jsonobj 
422          else: 
423              for db in dbs: 
424                  docs = [] 
425                  docs = [doc.doc(db) for doc in apps] 
426                  try: 
427                      db.save_docs(docs) 
428                  except BulkSaveError, e: 
429                      docs1 = [] 
430                      for doc in e.errors: 
431                          try: 
432                              doc['_rev'] = db.last_rev(doc['_id']) 
433                              docs1.append(doc) 
434                          except ResourceNotFound: 
435                              pass 
436                      if docs1: 
437                          db.save_docs(docs1) 
 438   
439   
440 -def pushdocs(path, dbs, atomic=True, export=False): 
 441      """ push multiple docs in a path """ 
442      if not isinstance(dbs, (list, tuple)): 
443          dbs = [dbs] 
444   
445      docs = [] 
446      for d in os.listdir(path): 
447          docdir = os.path.join(path, d) 
448          if docdir.startswith('.'): 
449              continue 
450          elif os.path.isfile(docdir): 
451              if d.endswith(".json"): 
452                  doc = utils.read_json(docdir) 
453                  docid, ext = os.path.splitext(d) 
454                  doc.setdefault('_id', docid) 
455                  doc.setdefault('couchapp', {}) 
456                  if not atomic: 
457                      for db in dbs: 
458                          db.save_doc(doc, force_update=True) 
459                  else: 
460                      docs.append(doc) 
461          else: 
462              doc = document(docdir, is_ddoc=False) 
463              if not atomic: 
464                  doc.push(dbs, atomic=False) 
465              else: 
466                  docs.append(doc) 
467      if docs: 
468          if export: 
469              docs1 = [] 
470              for doc in docs: 
471                  if hasattr(doc, 'doc'): 
472                      docs1.append(doc.doc()) 
473                  else: 
474                      docs1.append(doc) 
475              jsonobj = {'docs': docs1} 
476              return jsonobj 
477          else: 
478              for db in dbs: 
479                  docs1 = [] 
480                  for doc in docs: 
481                      if hasattr(doc, 'doc'): 
482                          docs1.append(doc.doc(db)) 
483                      else: 
484                          newdoc = doc.copy() 
485                          try: 
486                              rev = db.last_rev(doc['_id']) 
487                              newdoc.update({'_rev': rev}) 
488                          except ResourceNotFound: 
489                              pass 
490                          docs1.append(newdoc) 
491                  try: 
492                      db.save_docs(docs1) 
493                  except BulkSaveError, e: 
494                       
495                      docs1 = [] 
496                      for doc in e.errors: 
497                          try: 
498                              doc['_rev'] = db.last_rev(doc['_id']) 
499                              docs1.append(doc) 
500                          except ResourceNotFound: 
501                              pass 
502                  if docs1: 
503                      db.save_docs(docs1) 
 504   
505 -def clone(db, docid, dest=None, rev=None): 
 506      """ 
507      Clone a CouchDB document to the fs. 
508   
509      """ 
510      if not dest: 
511          dest = docid 
512   
513      path = os.path.normpath(os.path.join(os.getcwd(), dest)) 
514      if not os.path.exists(path): 
515          os.makedirs(path) 
516   
517      if not rev: 
518          doc = db.open_doc(docid) 
519      else: 
520          doc = db.open_doc(docid, rev=rev) 
521      docid = doc['_id'] 
522   
523   
524      metadata = doc.get('couchapp', {}) 
525   
526       
527      manifest = metadata.get('manifest', {}) 
528   
529       
530      signatures = metadata.get('signatures', {}) 
531   
532       
533      objects = metadata.get('objects', {}) 
534   
535       
536      if manifest: 
537          for filename in manifest: 
538              logger.debug("clone property: %s" % filename) 
539              filepath = os.path.join(path, filename) 
540              if filename.endswith('/'): 
541                  if not os.path.isdir(filepath): 
542                      os.makedirs(filepath) 
543              elif filename == "couchapp.json": 
544                  continue 
545              else: 
546                  parts = utils.split_path(filename) 
547                  fname = parts.pop() 
548                  v = doc 
549                  while 1: 
550                      try: 
551                          for key in parts: 
552                              v = v[key] 
553                      except KeyError: 
554                          break 
555                       
556                      last_key, ext = os.path.splitext(fname) 
557   
558                       
559                      try: 
560                          content = v[last_key] 
561                      except KeyError: 
562                          break 
563   
564   
565                      if isinstance(content, basestring): 
566                          _ref = md5(utils.to_bytestring(content)).hexdigest() 
567                          if objects and _ref in objects: 
568                              content = objects[_ref] 
569   
570                          if content.startswith('base64-encoded;'): 
571                              content = base64.b64decode(content[15:]) 
572   
573                      if fname.endswith('.json'): 
574                          content = utils.json.dumps(content).encode('utf-8') 
575   
576                      del v[last_key] 
577   
578                       
579                      filedir = os.path.dirname(filepath) 
580                      if not os.path.isdir(filedir): 
581                          os.makedirs(filedir) 
582   
583                      utils.write_content(filepath, content) 
584   
585                       
586                      temp = doc 
587                      for key2 in parts: 
588                          if key2 == key: 
589                              if not temp[key2]: 
590                                  del temp[key2] 
591                              break 
592                          temp = temp[key2] 
593   
594   
595       
596       
597      for key in doc.iterkeys(): 
598          if key.startswith('_'): 
599              continue 
600          elif key in ('couchapp'): 
601              app_meta = copy.deepcopy(doc['couchapp']) 
602              if 'signatures' in app_meta: 
603                  del app_meta['signatures'] 
604              if 'manifest' in app_meta: 
605                  del app_meta['manifest'] 
606              if 'objects' in app_meta: 
607                  del app_meta['objects'] 
608              if 'length' in app_meta: 
609                  del app_meta['length'] 
610              if app_meta: 
611                  couchapp_file = os.path.join(path, 'couchapp.json') 
612                  utils.write_json(couchapp_file, app_meta) 
613          elif key in ('views'): 
614              vs_dir = os.path.join(path, key) 
615              if not os.path.isdir(vs_dir): 
616                  os.makedirs(vs_dir) 
617              for vsname, vs_item in doc[key].iteritems(): 
618                  vs_item_dir = os.path.join(vs_dir, vsname) 
619                  if not os.path.isdir(vs_item_dir): 
620                      os.makedirs(vs_item_dir) 
621                  for func_name, func in vs_item.iteritems(): 
622                      filename = os.path.join(vs_item_dir, '%s.js' % 
623                              func_name) 
624                      utils.write_content(filename, func) 
625                      logger.warning("clone view not in manifest: %s" % filename) 
626          elif key in ('shows', 'lists', 'filter', 'update'): 
627              showpath = os.path.join(path, key) 
628              if not os.path.isdir(showpath): 
629                  os.makedirs(showpath) 
630              for func_name, func in doc[key].iteritems(): 
631                  filename = os.path.join(showpath, '%s.js' % 
632                          func_name) 
633                  utils.write_content(filename, func) 
634                  logger.warning( 
635                      "clone show or list not in manifest: %s" % filename) 
636          else: 
637              filedir = os.path.join(path, key) 
638              if os.path.exists(filedir): 
639                  continue 
640              else: 
641                  logger.warning("clone property not in manifest: %s" % key) 
642                  if isinstance(doc[key], (list, tuple,)): 
643                      utils.write_json(filedir + ".json", doc[key]) 
644                  elif isinstance(doc[key], dict): 
645                      if not os.path.isdir(filedir): 
646                          os.makedirs(filedir) 
647                      for field, value in doc[key].iteritems(): 
648                          fieldpath = os.path.join(filedir, field) 
649                          if isinstance(value, basestring): 
650                              if value.startswith('base64-encoded;'): 
651                                  value = base64.b64decode(content[15:]) 
652                              utils.write_content(fieldpath, value) 
653                          else: 
654                              utils.write_json(fieldpath + '.json', value) 
655                  else: 
656                      value = doc[key] 
657                      if not isinstance(value, basestring): 
658                          value = str(value) 
659                      utils.write_content(filedir, value) 
660   
661       
662      idfile = os.path.join(path, '_id') 
663      utils.write_content(idfile, doc['_id']) 
664   
665      utils.write_json(os.path.join(path, '.couchapprc'), {}) 
666   
667      if '_attachments' in doc:   
668          attachdir = os.path.join(path, '_attachments') 
669          if not os.path.isdir(attachdir): 
670              os.makedirs(attachdir) 
671   
672          for filename in doc['_attachments'].iterkeys(): 
673              if filename.startswith('vendor'): 
674                  attach_parts = utils.split_path(filename) 
675                  vendor_attachdir = os.path.join(path, attach_parts.pop(0), 
676                          attach_parts.pop(0), '_attachments') 
677                  filepath = os.path.join(vendor_attachdir, *attach_parts) 
678              else: 
679                  filepath = os.path.join(attachdir, filename) 
680              filepath = _replace_slash(filepath) 
681              currentdir = os.path.dirname(filepath) 
682              if not os.path.isdir(currentdir): 
683                  os.makedirs(currentdir) 
684   
685              if signatures.get(filename) != utils.sign_file(filepath): 
686                  stream = db.fetch_attachment(docid, filename, stream=True) 
687                  with open(filepath, 'wb') as f: 
688                      for chunk in stream: 
689                          f.write(chunk) 
690                  logger.debug("clone attachment: %s" % filename) 
691   
692      logger.debug("%s/%s cloned in %s" % (db.uri, docid, dest)) 
 693   
695      """ Clone a design document from it's url like couchapp does. 
696      """ 
697      try: 
698          dburl, docid = source.split('_design/') 
699      except ValueError: 
700          raise DesignerError("%s isn't a valid source" % source) 
701   
702      db = client.Database(dburl[:-1], create=False) 
703      clone(db, docid, dest, rev=rev) 
 704