Home | History | Annotate | Download | only in sourcedr
      1 #!/usr/bin/env python3
      2 
      3 """SourceDR project configurations and databases.
      4 
      5 `Project` class holds configuration files, review databases, pattern databases,
      6 and `codesearch` index files.
      7 """
      8 
      9 import collections
     10 import json
     11 import os
     12 import shutil
     13 
     14 from sourcedr.codesearch import CodeSearch, PathFilter
     15 from sourcedr.pattern_db import PatternDB
     16 from sourcedr.review_db import ReviewDB
     17 from sourcedr.utils import LockedFile
     18 
     19 
     20 class Config(object):
     21     """SourceDR project configuration file."""
     22 
     23     DEFAULT_NAME = 'sourcedr.json'
     24 
     25     _PATH_TRAVERSAL_ATTRS = (
     26             'file_ext_blacklist', 'file_name_blacklist',
     27             'path_component_blacklist')
     28 
     29 
     30     @classmethod
     31     def get_default_path(cls, project_dir):
     32         """Get the default path of the configuration file under a project
     33         directory."""
     34         return os.path.join(project_dir, cls.DEFAULT_NAME)
     35 
     36 
     37     def __init__(self, path):
     38         self.path = path
     39 
     40         self.source_dir = None
     41         self.file_ext_blacklist = set()
     42         self.file_name_blacklist = set()
     43         self.path_component_blacklist = set()
     44 
     45 
     46     def load(self):
     47         """Load the project configuration from the JSON file."""
     48         with open(self.path, 'r') as config_fp:
     49             config_json = json.load(config_fp)
     50             for key, value in config_json.items():
     51                 if key == 'source_dir':
     52                     self.source_dir = value
     53                 elif key in self._PATH_TRAVERSAL_ATTRS:
     54                     setattr(self, key, set(value))
     55                 else:
     56                     raise ValueError('unknown config name: ' + key)
     57 
     58 
     59     def save(self):
     60         """Save the project configuration to the JSON file."""
     61         with LockedFile(self.path, 'x') as config_fp:
     62             config = collections.OrderedDict()
     63             config['source_dir'] = self.source_dir
     64             for key in self._PATH_TRAVERSAL_ATTRS:
     65                 config[key] = sorted(getattr(self, key))
     66             json.dump(config, config_fp, indent=2)
     67 
     68 
     69 class Project(object):
     70     """SourceDR project configuration files and databases."""
     71 
     72     def __init__(self, project_dir):
     73         """Load a project from a given project directory."""
     74 
     75         project_dir = os.path.abspath(project_dir)
     76         self.project_dir = project_dir
     77 
     78         if not os.path.isdir(project_dir):
     79             raise ValueError('project directory not found: ' + project_dir)
     80 
     81         # Load configuration files
     82         config_path = Config.get_default_path(project_dir)
     83         self.config = Config(config_path)
     84         self.config.load()
     85 
     86         # Recalculate source directory
     87         self.source_dir = os.path.abspath(
     88                 os.path.join(project_dir, self.config.source_dir))
     89 
     90         # csearchindex file
     91         path_filter = PathFilter(self.config.file_ext_blacklist,
     92                                  self.config.file_name_blacklist,
     93                                  self.config.path_component_blacklist)
     94         csearch_index_path = CodeSearch.get_default_path(project_dir)
     95         self.codesearch = CodeSearch(self.source_dir, csearch_index_path,
     96                                      path_filter)
     97         self.codesearch.add_default_filters()
     98 
     99         # Review database file
    100         review_db_path = ReviewDB.get_default_path(project_dir)
    101         self.review_db = ReviewDB(review_db_path, self.codesearch)
    102 
    103         # Pattern database file
    104         pattern_db_path = PatternDB.get_default_path(project_dir)
    105         self.pattern_db = PatternDB(pattern_db_path)
    106 
    107         # Sanity checks
    108         self._check_source_dir()
    109         self._check_lock_files()
    110 
    111 
    112     def update_csearch_index(self, remove_existing_index):
    113         """Create or update codesearch index."""
    114         self.codesearch.build_index(remove_existing_index)
    115 
    116 
    117     def update_review_db(self):
    118         """Update the entries in the review database."""
    119         patterns, is_regexs = self.pattern_db.load()
    120         self.review_db.find(patterns, is_regexs)
    121 
    122 
    123     def _check_source_dir(self):
    124         """Check the availability of the source directory."""
    125         if not os.path.isdir(self.source_dir):
    126             raise ValueError('source directory not found: ' + self.source_dir)
    127 
    128 
    129     def _check_lock_files(self):
    130         """Check whether there are some lock files."""
    131         for path in (self.config.path, self.review_db.path,
    132                      self.pattern_db.path):
    133             if LockedFile.is_locked(path):
    134                 raise ValueError('file locked: ' + path)
    135 
    136 
    137     @classmethod
    138     def create_project_dir(cls, project_dir, source_dir):
    139         """Create a directory for a new project and setup default
    140         configurations."""
    141 
    142         if not os.path.isdir(source_dir):
    143             raise ValueError('source directory not found: ' + source_dir)
    144 
    145         os.makedirs(project_dir, exist_ok=True)
    146 
    147         # Compute the relative path between project_dir and source_dir
    148         project_dir = os.path.abspath(project_dir)
    149         source_dir = os.path.relpath(os.path.abspath(source_dir), project_dir)
    150 
    151         # Copy default files
    152         defaults_dir = os.path.join(os.path.dirname(__file__), 'defaults')
    153         for name in (Config.DEFAULT_NAME, PatternDB.DEFAULT_NAME):
    154             shutil.copyfile(os.path.join(defaults_dir, name),
    155                             os.path.join(project_dir, name))
    156 
    157         # Update the source directory in the configuration file
    158         config_path = Config.get_default_path(project_dir)
    159         config = Config(config_path)
    160         config.load()
    161         config.source_dir = source_dir
    162         config.save()
    163 
    164         return Project(project_dir)
    165 
    166 
    167     @classmethod
    168     def get_or_create_project_dir(cls, project_dir, source_dir):
    169         config_file_path = Config.get_default_path(project_dir)
    170         if os.path.exists(config_file_path):
    171             return Project(project_dir)
    172         else:
    173             return cls.create_project_dir(project_dir, source_dir)
    174