class ElbeDB:

    # pylint: disable=too-many-public-methods

    db_path = '/var/cache/elbe'
    db_location = 'sqlite:///' + db_path + '/elbe.db'

    def __init__(self):
        engine = create_engine(self.__class__.db_location,
                               connect_args={'timeout': 30})
        Base.metadata.create_all(engine)
        smaker = sessionmaker(bind=engine)
        self.session = scoped_session(smaker)

    def list_users(self):
        with session_scope(self.session) as s:
            res = s.query(User).all()
            ret = []
            for u in res:
                ret.append(UserData(u))
            return ret

    def list_projects(self):
        with session_scope(self.session) as s:
            res = s.query(Project).all()
            ret = []
            for p in res:
                ret.append(ProjectData(p))
            return ret

    def list_projects_of(self, userid):
        with session_scope(self.session) as s:
            res = s.query(Project).filter(Project.owner_id == userid).all()
            ret = []
            for p in res:
                ret.append(ProjectData(p))
            return ret

    def get_project_data(self, builddir):
        # Can throw: ElbeDBError
        if not os.path.exists(builddir):
            raise ElbeDBError("project directory does not exist")

        with session_scope(self.session) as s:
            try:
                p = s.query(Project).filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            return ProjectData(p)

    def set_postbuild(self, builddir, postbuild_file):
        if not os.path.exists(builddir):
            raise ElbeDBError("project directory does not exist")

        with session_scope(self.session) as s:
            p = None
            try:
                p = s.query(Project). \
                    filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            if p.status == "busy":
                raise ElbeDBError(
                    "cannot set postbuild file while project %s is busy" %
                    builddir)

            p.edit = datetime.utcnow()

            with open(builddir + "/postbuild.sh", 'w') as dst:
                copyfileobj(postbuild_file, dst)

            os.chmod(builddir + "/postbuild.sh", 0o755)
            dos2unix(builddir + "/postbuild.sh")

            return _update_project_file(s, builddir,
                "postbuild.sh", "application/sh", "postbuild script")

    def set_savesh(self, builddir, savesh_file):
        if not os.path.exists(builddir):
            raise ElbeDBError("project directory does not exist")

        with session_scope(self.session) as s:
            p = None
            try:
                p = s.query(Project). \
                    filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            if p.status == "busy":
                raise ElbeDBError(
                    "cannot set savesh file while project %s is busy" %
                    builddir)

            p.edit = datetime.utcnow()
            if p.status == "empty_project" or p.status == "build_failed":
                p.status = "needs_build"
            elif p.status == "build_done":
                p.status = "has_changes"

            with open(builddir + "/save.sh", 'w') as dst:
                copyfileobj(savesh_file, dst)

            os.chmod(builddir + "/save.sh", 0o755)
            dos2unix(builddir + "/save.sh")

            return _update_project_file(
                s, builddir,
                "save.sh", "application/sh", "version save script")

    def set_presh(self, builddir, presh_file):
        if not os.path.exists(builddir):
            raise ElbeDBError("project directory does not exist")

        with session_scope(self.session) as s:
            p = None
            try:
                p = s.query(Project). \
                    filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            if p.status == "busy":
                raise ElbeDBError(
                    "cannot set presh file while project %s is busy" %
                    builddir)

            p.edit = datetime.utcnow()
            if p.status == "empty_project" or p.status == "build_failed":
                p.status = "needs_build"
            elif p.status == "build_done":
                p.status = "has_changes"

            with open(builddir + "/pre.sh", 'w') as dst:
                copyfileobj(presh_file, dst)

            dos2unix(builddir + "/pre.sh")

            return _update_project_file(
                s, builddir, "pre.sh", "application/sh", "pre install script")

    def set_postsh(self, builddir, postsh_file):
        if not os.path.exists(builddir):
            raise ElbeDBError("project directory does not exist")

        with session_scope(self.session) as s:
            p = None
            try:
                p = s.query(Project). \
                    filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            if p.status == "busy":
                raise ElbeDBError(
                    "cannot set postsh file while project %s is busy" %
                    builddir)

            p.edit = datetime.utcnow()
            if p.status == "empty_project" or p.status == "build_failed":
                p.status = "needs_build"
            elif p.status == "build_done":
                p.status = "has_changes"

            with open(builddir + "/post.sh", 'w') as dst:
                copyfileobj(postsh_file, dst)

            dos2unix(builddir + "/post.sh")

            return _update_project_file(
                s, builddir,
                "post.sh", "application/sh", "post install script")

    def set_xml(self, builddir, xml_file):
        # This method can throw: ElbeDBError, ValidationError, OSError

        if not os.path.exists(builddir):
            raise ElbeDBError("project directory does not exist")

        srcxml_fname = os.path.join(builddir, "source.xml")

        if xml_file is None:
            xml_file = srcxml_fname

        with session_scope(self.session) as s:
            p = None
            try:
                p = s.query(Project). \
                    filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            if p.status == "busy":
                raise ElbeDBError(
                    "cannot set XML file while project %s is busy" %
                    builddir)

            xml = ElbeXML(
                xml_file,
                url_validation=ValidationMode.NO_CHECK)  # ValidationError

            p.name = xml.text("project/name")
            p.version = xml.text("project/version")
            p.edit = datetime.utcnow()
            if p.status == "empty_project" or p.status == "build_failed":
                p.status = "needs_build"
            elif p.status == "build_done":
                p.status = "has_changes"

            if xml_file != srcxml_fname:
                copyfile(xml_file, srcxml_fname)  # OSError

            _update_project_file(
                s,
                builddir,
                "source.xml",
                "application/xml",
                "ELBE recipe of the project")

    # TODO what about source.xml ? stored always in db ? version management ?
    #       build/needs_build state ? locking ?

    def create_project(self, builddir, owner_id=None):
        # Throws: ElbeDBError, OSError
        directory_created = False

        try:
            with session_scope(self.session) as s:
                if s.query(Project).\
                        filter(Project.builddir == builddir).count() > 0:
                    raise ElbeDBError("project %s already exists in database" %
                                      builddir)

                try:
                    os.makedirs(builddir)  # OSError
                    directory_created = True
                except OSError as e:
                    if e.errno == errno.EEXIST:
                        raise ElbeDBError("project directory %s already exists" %
                                          builddir)
                    raise

                p = Project(builddir=builddir, status="empty_project",
                            owner_id=owner_id)
                s.add(p)
        except BaseException:
            # If we have created a project directory but could not add the
            # project to the database, remove the otherwise orphaned directory
            # again.
            if directory_created:
                rmtree(builddir)  # OSError
            raise

    def del_project(self, builddir):
        # Throws: ElbeDBError, OSError
        p = None
        with session_scope(self.session) as s:
            try:
                p = s.query(Project).filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            if p.status == "busy":
                raise ElbeDBError(
                    "cannot delete project %s while it is busy" %
                    builddir)

            if os.path.exists(builddir):
                # delete project in background to avoid blocking caller for a
                # long time if the project is huge
                t = Thread(target=rmtree, args=[builddir])
                t.start()

            s.query(ProjectVersion).\
                filter(ProjectVersion.builddir == builddir).delete()

            s.query(ProjectFile).\
                filter(ProjectFile.builddir == builddir).delete()

            s.delete(p)

    def reset_project(self, builddir, clean):
        # Throws: ElbeDBError, OSError
        with session_scope(self.session) as s:
            try:
                p = s.query(Project).filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            sourcexmlpath = os.path.join(builddir, "source.xml")
            if os.path.exists(sourcexmlpath):
                p.status = "needs_build"
            else:
                p.status = "empty_project"

        if clean:
            targetpath = os.path.join(builddir, "target")
            if os.path.exists(targetpath):
                rmtree(targetpath)      # OSError

            chrootpath = os.path.join(builddir, "chroot")
            if os.path.exists(chrootpath):
                rmtree(chrootpath)      # OSError

    def save_project(self, ep):
        # TODO: Recover in case writing the XML file or commiting the
        # database entry fails
        project = None

        with session_scope(self.session) as s:
            try:
                project = s.query(Project).filter(
                    Project.builddir == ep.builddir).one()
            except NoResultFound:
                pass

            if not os.path.exists(ep.builddir):
                os.makedirs(ep.builddir)
            if not os.path.isfile(ep.builddir + "/source.xml") and ep.xml:
                ep.xml.xml.write(ep.builddir + "/source.xml")

            with open(ep.builddir + "/source.xml") as xml_file:
                xml_str = xml_file.read()
                if not project:
                    project = Project(name=ep.xml.text("project/name"),
                                      version=ep.xml.text("project/version"),
                                      builddir=ep.builddir,
                                      xml=xml_str)
                    s.add(project)
                else:
                    project.edit = datetime.utcnow()
                    project.version = ep.xml.text("project/version")
                    project.xml = xml_str

    def load_project(
            self,
            builddir,
            url_validation=ValidationMode.CHECK_ALL):

        # pass exceptions if hook-scripts can't be loaded (they're optional)
        postbuild_file = None
        try:
            postbuild = self.get_project_file(builddir, 'postbuild.sh')
            postbuild_file = postbuild.builddir + '/' + postbuild.name
        except ElbeDBError:
            pass

        presh_file = None
        try:
            presh_handle = self.get_project_file(builddir, 'pre.sh')
            presh_file = presh_handle.builddir + '/' + presh_handle.name
        except ElbeDBError:
            pass

        postsh_file = None
        try:
            postsh_handle = self.get_project_file(builddir, 'post.sh')
            postsh_file = postsh_handle.builddir + '/' + postsh_handle.name
        except ElbeDBError:
            pass

        savesh_file = None
        try:
            savesh_handle = self.get_project_file(builddir, 'save.sh')
            savesh_file = savesh_handle.builddir + '/' + savesh_handle.name
        except ElbeDBError:
            pass

        with session_scope(self.session) as s:
            try:
                p = s.query(Project). \
                    filter(Project.builddir == builddir).one()

                return ElbeProject(p.builddir, name=p.name,
                                   postbuild_file=postbuild_file,
                                   presh_file=presh_file,
                                   postsh_file=postsh_file,
                                   savesh_file=savesh_file,
                                   url_validation=url_validation)
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

    def set_busy(self, builddir, allowed_status):
        assert "busy" not in allowed_status
        with session_scope(self.session) as s:
            try:
                p = s.query(Project).with_for_update(). \
                    filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            if p.status not in allowed_status:
                raise ElbeDBError("project: " + builddir +
                                  " set_busy: invalid status: " + p.status)

            old_status = p.status
            p.status = "busy"
            return old_status

    def is_busy(self, builddir):
        with session_scope(self.session) as s:
            try:
                p = s.query(Project).filter(Project.builddir == builddir). \
                    one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            return p.status == "busy"

    def reset_busy(self, builddir, new_status):
        assert new_status in ("has_changes", "build_done", "build_failed")

        with session_scope(self.session) as s:
            try:
                p = s.query(Project).with_for_update(). \
                    filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            if p.status != "busy":
                raise ElbeDBError(
                    "project: " +
                    builddir +
                    " reset_busy: invalid status: " +
                    p.status)

            p.status = new_status

    def has_changes(self, builddir):
        with session_scope(self.session) as s:
            try:
                p = s.query(Project).filter(Project.builddir == builddir). \
                    one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            return p.status == "has_changes"

    def get_owner_id(self, builddir):
        with session_scope(self.session) as s:
            try:
                p = s.query(Project).filter(Project.builddir == builddir).\
                    one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            if p.owner_id is None:
                return None

            return int(p.owner_id)

    def set_project_version(self, builddir, new_version=None):
        if new_version == "":
            raise ElbeDBError("version number must not be empty")

        if not re.match("^[A-Za-z0-9_.-]{1,25}$", new_version):
            raise ElbeDBError(
                "version number must contain valid characters [A-Za-z0-9_-.]")

        with session_scope(self.session) as s:
            try:
                p = s.query(Project).filter(Project.builddir == builddir).\
                    one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            if p.status == "empty_project" or p.status == "busy":
                raise ElbeDBError(
                    "project: " +
                    builddir +
                    " set_project_version: invalid status: " +
                    p.status)

            xmlpath = os.path.join(builddir, "source.xml")
            xml = ElbeXML(xmlpath, url_validation=ValidationMode.NO_CHECK)

            if new_version is not None:
                xml.node("/project/version").set_text(new_version)
                xml.xml.write(xmlpath)

            p.version = xml.text("/project/version")

    def list_project_versions(self, builddir):
        with session_scope(self.session) as s:
            try:
                p = s.query(Project).filter(Project.builddir == builddir).\
                    one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            return [ProjectVersionData(v) for v in p.versions]

    def save_version(self, builddir, description=None):
        with session_scope(self.session) as s:
            try:
                p = s.query(Project).filter(Project.builddir == builddir).\
                    one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            assert p.status == "busy"

            sourcexmlpath = os.path.join(builddir, "source.xml")
            sourcexml = ElbeXML(sourcexmlpath,
                                url_validation=ValidationMode.NO_CHECK)

            version = sourcexml.text("project/version")
            if s.query(ProjectVersion).\
                    filter(ProjectVersion.builddir == builddir).\
                    filter(ProjectVersion.version == version).count() > 0:
                raise ElbeDBError(
                    "Version %s already exists for project in %s, "
                    "please change version number first" %
                    (version, builddir)
                )

            versionxmlname = get_versioned_filename(p.name, version,
                                                    ".version.xml")
            versionxmlpath = os.path.join(builddir, versionxmlname)
            copyfile(sourcexmlpath, versionxmlpath)

            v = ProjectVersion(builddir=builddir,
                               version=version,
                               description=description)
            s.add(v)

            _update_project_file(s, builddir, versionxmlname,
                                      "application/xml",
                                      "source.xml for version %s" % version)

    def set_version_description(self, builddir, version, description):
        with session_scope(self.session) as s:
            try:
                v = s.query(ProjectVersion).\
                    filter(ProjectVersion.builddir == builddir).\
                    filter(ProjectVersion.version == version).one()
            except NoResultFound:
                raise ElbeDBError(
                    "no such project version: %s (version %s)" %
                    (builddir, version))

            v.description = description

    def checkout_version_xml(self, builddir, version):
        with session_scope(self.session) as s:
            try:
                v = s.query(ProjectVersion).\
                    filter(ProjectVersion.builddir == builddir).\
                    filter(ProjectVersion.version == version).one()
            except NoResultFound:
                raise ElbeDBError(
                    "no such project version: %s (version %s)" %
                    (builddir, version))

            assert v.project.status == "busy"

            sourcexmlpath = os.path.join(builddir, "source.xml")
            versionxmlname = get_versioned_filename(v.project.name, version,
                                                    ".version.xml")
            versionxmlpath = os.path.join(builddir, versionxmlname)

            copyfile(versionxmlpath, sourcexmlpath)
            v.project.version = version

    def del_version(self, builddir, version, force=False):
        with session_scope(self.session) as s:
            try:
                v = s.query(ProjectVersion).\
                    filter(ProjectVersion.builddir == builddir).\
                    filter(ProjectVersion.version == version).one()
            except NoResultFound:
                raise ElbeDBError(
                    "no such project version: %s (version %s)" %
                    (builddir, version))

            if not force:
                if v.project.status == "busy":
                    raise ElbeDBError(
                        "cannot delete version of project in %s while "
                        "it is busy" % builddir)

            xmlname = get_versioned_filename(v.project.name, version,
                                             ".version.xml")
            xmlpath = os.path.join(builddir, xmlname)
            os.remove(xmlpath)
            s.delete(v)

            s.query(ProjectFile).filter(ProjectFile.builddir == builddir).\
                filter(ProjectFile.name == xmlname).delete()

    def get_version_xml(self, builddir, version):
        with session_scope(self.session) as s:
            try:
                v = s.query(ProjectVersion).\
                    filter(ProjectVersion.builddir == builddir).\
                    filter(ProjectVersion.version == version).one()
            except NoResultFound:
                raise ElbeDBError("no such project version: %s (version %s)" %
                                  (builddir, version))

            xmlname = get_versioned_filename(v.project.name, version,
                                             ".version.xml")
            return os.path.join(builddir, xmlname)

    def get_project_files(self, builddir):
        # Can throw: ElbeDBError
        with session_scope(self.session) as s:
            try:
                p = s.query(Project).filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            if p.status == "busy":
                raise ElbeDBError(
                    "project: " +
                    builddir +
                    " get_project_files: invalid status: " +
                    p.status)

            return [ProjectFileData(f) for f in p.files]

    def get_project_file(self, builddir, name):
        with session_scope(self.session) as s:
            try:
                p = s.query(Project).filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            if p.status == "busy":
                raise ElbeDBError(
                    "project: " +
                    builddir +
                    " get_project_file: invalid status: " +
                    p.status)

            try:
                f = s.query(ProjectFile).\
                    filter(ProjectFile.builddir == builddir).\
                    filter(ProjectFile.name == name).one()
            except NoResultFound:
                raise ElbeDBError(
                    "no file %s in project %s registered" %
                    (name, builddir))

            return ProjectFileData(f)

    def add_project_file(self, builddir, name, mime_type, description=None):
        with session_scope(self.session) as s:
            try:
                s.query(Project).filter(Project.builddir == builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    builddir)

            _update_project_file(s, builddir, name, mime_type,
                                      description)

    def update_project_files(self, ep):
        with session_scope(self.session) as s:
            try:
                p = s.query(Project).\
                    filter(Project.builddir == ep.builddir).one()
            except NoResultFound:
                raise ElbeDBError(
                    "project %s is not registered in the database" %
                    ep.builddir)

            # Delete no longer existing files from the database
            files = s.query(ProjectFile).\
                filter(ProjectFile.builddir == ep.builddir).all()
            for f in files:
                if not os.path.isfile(os.path.join(ep.builddir, f.name)):
                    s.delete(f)

            # Add images from the given ElbeProject
            if ep.targetfs:

                images = set(ep.targetfs.images or [])
                for img in images:
                    _update_project_file(
                        s, p.builddir, img,
                        "application/octet-stream", "Image")

            # Add other generated files
            _update_project_file(s, p.builddir, "source.xml",
                                      "application/xml",
                                      "Current source.xml of the project")

            for name in ["chroot", "target", "sysroot-target", "sysroot-host"]:

                _update_project_file(s, p.builddir, "licence-%s.txt" % name,
                                     "text/plain; charset=utf-8",
                                     "License file")

                _update_project_file(s, p.builddir, "licence-%s.xml" % name,
                                     "application/xml",
                                     "xml License file")

            _update_project_file(s, p.builddir, "validation.txt",
                                      "text/plain; charset=utf-8",
                                      "Package list validation result")

            _update_project_file(s, p.builddir, "elbe-report.txt",
                                      "text/plain; charset=utf-8",
                                      "Report")

            _update_project_file(s, p.builddir, "log.txt",
                                      "text/plain; charset=utf-8",
                                      "Log file")

            _update_project_file(s, p.builddir, "sysroot.tar.xz",
                                      "application/x-xz-compressed-tar",
                                      "sysroot for cross-toolchains")

            sdk = glob.glob(os.path.join(p.builddir, "setup-elbe-sdk-*.sh"))
            try:
                # throws index error if no  setup-elbe-sdk-* file exists
                # that's ok because it might not yet been built
                sdkname = sdk[0].split('/')[-1]

                _update_project_file(s, p.builddir, sdkname,
                                        "application/x-shellscript",
                                        "SDK Installer")
            except IndexError:
                pass

            _update_project_file(s, p.builddir, "chroot.tar.xz",
                                      "application/x-xz-compressed-tar",
                                      "chroot for 'native' development")

            # Add Repository iso images
            for img in ep.repo_images:
                name = os.path.basename(img)
                _update_project_file(s, p.builddir, name,
                                          "application/octet-stream",
                                          "Repository IsoImage")

            # Scan pbuilder/build directory if that exists
            if os.path.exists(os.path.join(p.builddir, "pbuilder", "result")):
                pbresult_path = os.path.join(p.builddir, "pbuilder", "result")
                pfile_path = os.path.join("pbuilder", "result")
            else:
                pbresult_path = os.path.join(p.builddir, "pbuilder_cross",
                                             "result")
                pfile_path = os.path.join("pbuilder_cross", "result")

            if os.path.isdir(pbresult_path):
                for f in os.listdir(pbresult_path):
                    pfile = os.path.join(pfile_path, f)
                    _update_project_file(s, p.builddir, pfile,
                                              "application/octet-stream",
                                              "Pbuilder artifact")


    def add_user(self, name, fullname, password, email, admin):

        # pylint: disable=too-many-arguments

        # encrypt is deprecated but hash is not available in jessie
        try:
            pwhash = pbkdf2_sha512.hash(password)
        except AttributeError:
            pwhash = pbkdf2_sha512.encrypt(password)

        u = User(name=name,
                 fullname=fullname,
                 pwhash=pwhash,
                 email=email,
                 admin=admin)

        with session_scope(self.session) as s:
            if s.query(User).filter(User.name == name).count() > 0:
                raise ElbeDBError("user %s already exists in the database" %
                                  name)
            s.add(u)

    def modify_user(self, userid, name, fullname, email, admin,
                    password=None):

        # pylint: disable=too-many-arguments

        with session_scope(self.session) as s:
            try:
                u = s.query(User).filter(User.id == userid).one()
            except NoResultFound:
                raise ElbeDBError("no user with id %i" % userid)

            # If a user name change is requested, check for uniqueness
            if name != u.name:
                if s.query(User).filter(User.name == name).count() > 0:
                    raise ElbeDBError(
                        "user %s already exists in the database" % name)

            u.name = name
            u.fullname = fullname
            u.email = email
            u.admin = admin

            # Update password only if given
            if password is not None:
                # encrypt is deprecated but hash is not available in jessie
                try:
                    u.pwhash = pbkdf2_sha512.hash(password)
                except AttributeError:
                    u.pwhash = pbkdf2_sha512.encrypt(password)

    def del_user(self, userid):
        with session_scope(self.session) as s:
            try:
                u = s.query(User).filter(User.id == userid).one()
            except NoResultFound:
                raise ElbeDBError("no user with id %i" % userid)

            # Get a list of all projects owned by the user to delete. Set their
            # owner to nobody and return them to the caller later, so it can
            # decide whether to keep the projects or delete them.
            orphaned_projects = s.query(Project).\
                filter(Project.owner_id == userid).all()
            projectlist = []
            for p in orphaned_projects:
                p.owner_id = None
                projectlist.append(ProjectData(p))

            # Now delete the user and return the list
            s.delete(u)
            return projectlist

    def validate_login(self, name, password):
        with session_scope(self.session) as s:
            # Find the user with the given name
            try:
                u = s.query(User).filter(User.name == name).one()
            except NoResultFound:
                raise InvalidLogin()

            # Check password, throw an exception if invalid
            if not pbkdf2_sha512.verify(password, u.pwhash):
                raise InvalidLogin()

            # Everything good, now return the user id to the caller
            return int(u.id)

    def is_admin(self, userid):
        with session_scope(self.session) as s:
            try:
                u = s.query(User).filter(User.id == userid).one()
            except NoResultFound:
                raise ElbeDBError("no user with id %i" % userid)

            return bool(u.admin)

    def get_username(self, userid):
        with session_scope(self.session) as s:
            try:
                u = s.query(User).filter(User.id == userid).one()
            except NoResultFound:
                raise ElbeDBError("no user with id %i" % userid)

            return str(u.name)

    def get_user_data(self, userid):
        with session_scope(self.session) as s:
            try:
                u = s.query(User).filter(User.id == userid).one()
            except NoResultFound:
                raise ElbeDBError("no user with id %i" % userid)

            return UserData(u)

    def get_user_id(self, name):
        with session_scope(self.session) as s:
            try:
                u = s.query(User).filter(User.name == name).one()
            except NoResultFound:
                raise ElbeDBError("no user with name %s" % name)

            return int(u.id)

    @classmethod
    def init_db(cls, name, fullname, password, email, admin):

        # pylint: disable=too-many-arguments

        if not os.path.exists(cls.db_path):
            try:
                os.makedirs(cls.db_path)
            except OSError as e:
                print(str(e))
                return

        db = ElbeDB()

        try:
            db.add_user(name, fullname, password, email, admin)
        except ElbeDBError as e:
            print(str(e)) def get_versioned_filename(name, version, suffix):
    return quote(name, ' ') + '_' + quote(version, ' ') + suffix class AsyncWorker(Thread):
    def __init__(self, db):
        Thread.__init__(self, name="AsyncWorker")
        self.db = db
        self.queue = Queue()
        self.start()

    def stop(self):
        self.queue.put(None)
        self.queue.join()
        self.join()

    def enqueue(self, job):
        job.enqueue(self.queue, self.db)

    def run(self):
        loop = True
        while loop:
            job = self.queue.get()
            if job is not None:
                with savecwd():
                    with elbe_logging({"projects":job.project.builddir}):
                        job.execute(self.db)
            else:
                loop = False
            self.queue.task_done() class BuildJob(AsyncWorkerJob):
    def __init__(self, project, build_bin, build_src, skip_pbuilder):
        AsyncWorkerJob.__init__(self, project)
        self.build_bin = build_bin
        self.build_src = build_src
        self.skip_pbuilder = skip_pbuilder

    def enqueue(self, queue, db):
        db.set_busy(self.project.builddir,
                    ["empty_project", "needs_build", "has_changes",
                     "build_done", "build_failed"])
        logging.info("Enqueueing project for build")
        AsyncWorkerJob.enqueue(self, queue, db)

    def execute(self, db):

        success = self.build_failed
        try:
            logging.info("Build started")
            self.project.build(skip_pkglist=False,
                               build_bin=self.build_bin,
                               build_sources=self.build_src,
                               skip_pbuild=self.skip_pbuilder)
        except (DebootstrapException, AptCacheCommitError, AptCacheUpdateError) as e:
            if isinstance(e, DebootstrapException):
                err = "Debootstrap failed to install the base rootfilesystem."
            elif isinstance(e, AptCacheCommitError):
                err = "Failed to commit the AptCache changes."
            elif isinstance(e, AptCacheUpdateError):
                err ="Failed to build the Apt Cache."

            logging.exception("%s\n"
                              "Probable cause might be:\n"
                              "  - Problems with internet connection\n"
                              "  - Broken mirrors\n", err)
        # pylint: disable=broad-except
        except Exception:
            logging.exception("Build failed")
        else:
            if read_maxlevel(self.project.builddir) >= logging.ERROR:
                logging.info("Build finished with Error")
            else:
                logging.info("Build finished successfully")
                success = self.build_done
        finally:
            db.update_project_files(self.project)
            db.reset_busy(self.project.builddir, success) class APTUpdateJob(AsyncWorkerJob):
    def __init__(self, project):
        AsyncWorkerJob.__init__(self, project)

    def enqueue(self, queue, db):
        db.set_busy(self.project.builddir,
                    ["build_done", "has_changes"])
        logging.info("Enqueueing project for APT cache update")
        AsyncWorkerJob.enqueue(self, queue, db)

    def execute(self, db):
        success = self.build_failed
        try:
            logging.info("APT cache update started")
            with self.project.buildenv:
                self.project.get_rpcaptcache().update()
        # pylint: disable=broad-except
        except Exception:
            logging.exception("APT cache update failed")
        else:
            if read_maxlevel(self.project.builddir) >= logging.ERROR:
                logging.info("APT cache update finished with Error")
            else:
                logging.info("APT cache update finished successfully")
                success = self.has_changes
        finally:
            db.reset_busy(self.project.builddir, success) class APTCommitJob(AsyncWorkerJob):
    def __init__(self, project):
        AsyncWorkerJob.__init__(self, project)

    def enqueue(self, queue, db):
        old_status = db.set_busy(self.project.builddir,
                                 ["build_done", "has_changes"])
        if self.project.get_rpcaptcache().get_changes():
            logging.info("Enqueueing project for package changes")
            AsyncWorkerJob.enqueue(self, queue, db)
        else:
            db.reset_busy(self.project.builddir, old_status)

    def execute(self, db):
        success = self.build_failed
        try:
            logging.info("Applying package changes")
            with self.project.buildenv:
                # Commit changes, update full package list and write
                # out new source.xml
                self.project.get_rpcaptcache().commit()
                dump_fullpkgs(self.project.xml,
                              self.project.buildenv.rfs,
                              self.project.get_rpcaptcache())

            sourcexmlpath = path.join(self.project.builddir,
                                      "source.xml")
            self.project.xml.xml.write(sourcexmlpath)
        # pylint: disable=broad-except
        except Exception:
            logging.exception("Applying package changes failed")
        else:
            if read_maxlevel(self.project.builddir) >= logging.ERROR:
                logging.info("Package changes applied with Error")
            else:
                logging.info("Package changes applied successfully")
                success = self.has_changes
        finally:
            db.reset_busy(self.project.builddir, success) class GenUpdateJob(AsyncWorkerJob):
    def __init__(self, project, base_version):
        AsyncWorkerJob.__init__(self, project)
        self.name = project.xml.text("/project/name")
        self.base_version = base_version
        self.current_version = project.xml.text("/project/version")
        self.old_status = None
        self.base_version_xml = None

    def enqueue(self, queue, db):
        self.old_status = db.set_busy(self.project.builddir,
                                      ["build_done", "has_changes"])
        self.base_version_xml = db.get_version_xml(self.project.builddir,
                                                   self.base_version)

        logging.info("Enqueueing project for generating update package")
        AsyncWorkerJob.enqueue(self, queue, db)

    def execute(self, db):
        upd_filename = self._gen_upd_filename()
        upd_pathname = path.join(self.project.builddir, upd_filename)

        logging.info("Generating update package")

        try:
            gen_update_pkg(self.project, self.base_version_xml, upd_pathname)
            logging.info("Update package generated successfully")
        # pylint: disable=broad-except
        except Exception:
            logging.exception("Generating update package failed")
        finally:
            # Update generation does not change the project, so we always
            # keep the old status
            db.add_project_file(self.project.builddir, upd_filename,
                                "application/octet-stream",
                                "Update package from %s to %s" %
                                (self.base_version, self.current_version))
            db.reset_busy(self.project.builddir, self.old_status)

    def _gen_upd_filename(self):
        filename = quote(self.name, ' ') + '_'
        filename += quote(self.base_version) + '_'
        filename += quote(self.current_version) + '.upd'

        return filename class SaveVersionJob(AsyncWorkerJob):
    def __init__(self, project, description):
        AsyncWorkerJob.__init__(self, project)
        self.description = description
        self.name = self.project.xml.text("project/name")
        self.version = self.project.xml.text("project/version")
        self.old_status = None

    def enqueue(self, queue, db):
        self.old_status = db.set_busy(self.project.builddir,
                                      ["build_done", "has_changes"])

        # Create the database entry now. This has the advantage that the
        # user will see an error message immediately, if he tries to use
        # the same version number twice. The time-consuming part is creating
        # the package archive, which is done in execute.
        try:
            db.save_version(self.project.builddir, self.description)
        # pylint: disable=broad-except
        except BaseException:
            db.reset_busy(self.project.builddir, self.old_status)
            raise

        if self.project.savesh_file:
            logging.info("save version script:")
            do(self.project.savesh_file + ' "%s %s %s"' % (
                self.project.builddir,
                self.project.xml.text("project/version"),
                self.project.xml.text("project/name")),
                allow_fail=True)

        logging.info("Enqueueing project to save package archive")
        AsyncWorkerJob.enqueue(self, queue, db)

    def execute(self, db):
        logging.info("Generating package archive")
        repodir = get_versioned_filename(self.name, self.version,
                                         ".pkgarchive")
        try:
            gen_binpkg_archive(self.project, repodir)
        # pylint: disable=broad-except
        except Exception:
            logging.exception("Saving version failed")
            db.del_version(self.project.builddir, self.version, force=True)
        else:
            logging.info("Version saved successfully")
        finally:
            db.reset_busy(self.project.builddir, self.old_status) class CheckoutVersionJob(AsyncWorkerJob):
    def __init__(self, project, version):
        AsyncWorkerJob.__init__(self, project)
        self.version = version
        self.name = self.project.xml.text("project/name")

    def enqueue(self, queue, db):
        old_status = db.set_busy(self.project.builddir,
                                 ["build_done", "has_changes", "build_failed"])

        # If old status was build_failed, just restore the source.xml of the
        # given version and restore the status, indicating that we need a
        # complete rebuild
        if old_status == "build_failed":
            logging.warning("Previous project status indicated a failed build\n"
                            "Just checking out the XML file.")

            try:
                db.checkout_version_xml(self.project.builddir, self.version)
                self.project.set_xml(None)
            finally:
                db.reset_busy(self.project.builddir, old_status)
            return

        # Otherwise, restore the source.xml of the given version and enqueue
        # the project for package archive checkout
        try:
            db.checkout_version_xml(self.project.builddir, self.version)
            self.project.set_xml(None)
        except BaseException:
            db.reset_busy(self.project.builddir, old_status)
            self.project.set_xml(None)
            raise

        logging.info("Enqueueing project for package archive checkout")
        AsyncWorkerJob.enqueue(self, queue, db)

    def execute(self, db):
        logging.info("Checking out package archive")
        repodir = get_versioned_filename(self.name, self.version,
                                         ".pkgarchive")
        success = self.build_failed
        try:
            checkout_binpkg_archive(self.project, repodir)
            logging.info("Package archive checked out successfully")
        # pylint: disable=broad-except
        except Exception:
            logging.exception("Checking out package archive failed")
        else:
            success = self.has_changes
        finally:
            db.reset_busy(self.project.builddir, success) class APTUpdUpgrJob(AsyncWorkerJob):
    def __init__(self, project):
        AsyncWorkerJob.__init__(self, project)

    def enqueue(self, queue, db):
        db.set_busy(self.project.builddir, ["build_done", "has_changes"])
        logging.info("Enqueueing project for APT update & upgrade")
        AsyncWorkerJob.enqueue(self, queue, db)

    def execute(self, db):
        success = self.build_failed
        try:
            logging.info("APT update started")
            with self.project.buildenv:
                self.project.get_rpcaptcache().update()
            logging.info("APT update finished, upgrade started")
            self.project.get_rpcaptcache().upgrade()
        # pylint: disable=broad-except
        except Exception:
            logging.exception("APT update & upgrade failed")
        else:
            if read_maxlevel(self.project.builddir) >= logging.ERROR:
                logging.info("APT upgrade finished with Error")
            else:
                logging.info("APT upgrade finished")
                success = self.has_changes
        finally:
            db.reset_busy(self.project.builddir, success) class BuildSysrootJob(AsyncWorkerJob):
    def __init__(self, project):
        AsyncWorkerJob.__init__(self, project)

    def enqueue(self, queue, db):
        db.set_busy(self.project.builddir,
                    ["empty_project", "needs_build", "has_changes",
                     "build_done", "build_failed"])
        logging.info("Enqueueing project for building sysroot")
        AsyncWorkerJob.enqueue(self, queue, db)

    def execute(self, db):
        success = self.build_failed
        try:
            logging.info("Build sysroot started")
            self.project.build_sysroot()
            db.update_project_files(self.project)
        # pylint: disable=broad-except
        except Exception:
            logging.exception("Build sysroot failed")
        else:
            if read_maxlevel(self.project.builddir) >= logging.ERROR:
                logging.info("Build finished with Error")
            else:
                logging.info("Build finished successfully")
                success = self.build_done
        finally:
            db.update_project_files(self.project)
            db.reset_busy(self.project.builddir, success) class PdebuildJob(AsyncWorkerJob):
    def __init__(self, project, cpuset=-1, profile="", cross=False):
        AsyncWorkerJob.__init__(self, project)
        self.cpuset=cpuset
        self.profile=profile
        self.cross=cross

    def enqueue(self, queue, db):
        db.set_busy(self.project.builddir,
                    ["empty_project", "needs_build", "has_changes",
                     "build_done", "build_failed"])
        logging.info("Enqueueing project for pdebuild")
        AsyncWorkerJob.enqueue(self, queue, db)

    def execute(self, db):
        success = self.build_failed
        try:
            logging.info("Pdebuild started")
            self.project.pdebuild(self.cpuset, self.profile, self.cross)
        # pylint: disable=broad-except
        except Exception:
            logging.exception("Pdebuild failed")
        else:
            if read_maxlevel(self.project.builddir) >= logging.ERROR:
                logging.info("Pdeb finished with Error")
            else:
                logging.info("Pdeb finished successfully")
                success = self.build_done
        finally:
            db.update_project_files(self.project)
            db.reset_busy(self.project.builddir, success) class CreatePbuilderJob(AsyncWorkerJob):
    def __init__(self, project, ccachesize, cross=False, noccache=False):
        AsyncWorkerJob.__init__(self, project)
        self.cross = cross
        self.noccache = noccache
        self.ccachesize = ccachesize

    def enqueue(self, queue, db):
        db.set_busy(self.project.builddir,
                    ["empty_project", "needs_build", "has_changes",
                     "build_done", "build_failed"])
        logging.info("Enqueueing project to have the pbuilder built")
        AsyncWorkerJob.enqueue(self, queue, db)

    def execute(self, db):
        success = self.build_failed
        try:
            logging.info("Building pbuilder started")
            self.project.create_pbuilder(self.cross, self.noccache,
                                         self.ccachesize)
        # pylint: disable=broad-except
        except Exception:
            logging.exception("Pbuilder failed")
        else:
            logging.info("Pbuilder finished successfully")
            success = self.build_done
        finally:
            db.update_project_files(self.project)
            db.reset_busy(self.project.builddir, success) class UpdatePbuilderJob(AsyncWorkerJob):
    def __init__(self, project):
        AsyncWorkerJob.__init__(self, project)

    def enqueue(self, queue, db):
        db.set_busy(self.project.builddir,
                    ["empty_project", "needs_build", "has_changes",
                     "build_done", "build_failed"])
        logging.info("Enqueueing project to update the pbuilder")
        AsyncWorkerJob.enqueue(self, queue, db)

    def execute(self, db):
        success = self.build_done
        try:
            logging.info("Updating pbuilder started")
            self.project.update_pbuilder()
        # pylint: disable=broad-except
        except Exception:
            db.update_project_files(self.project)
            logging.exception("update Pbuilder failed")
        else:
            if read_maxlevel(self.project.builddir) >= logging.ERROR:
                logging.info("Updating Pbuilder finished with Error")
            else:
                logging.info("Updating Pbuilder finished successfully")
                success = self.build_done
        finally:
            db.reset_busy(self.project.builddir, success) class BuildChrootTarJob(AsyncWorkerJob):
    def __init__(self, project):
        AsyncWorkerJob.__init__(self, project)

    def enqueue(self, queue, db):
        db.set_busy(self.project.builddir,
                    ["empty_project", "needs_build", "has_changes",
                     "build_done", "build_failed"])
        logging.info("Enqueueing project for building croot tar")
        AsyncWorkerJob.enqueue(self, queue, db)

    def execute(self, db):
        success = self.build_failed
        try:
            logging.info("Build chroot tarball started")
            self.project.build_chroottarball()
        # pylint: disable=broad-except
        except Exception:
            logging.exception("Build chrroot tarball failed")
        else:
            if read_maxlevel(self.project.builddir) >= logging.ERROR:
                logging.info("Build finished with Error")
            else:
                logging.info("Build finished successfully")
                success = self.build_done
        finally:
            db.update_project_files(self.project)
            db.reset_busy(self.project.builddir, success) class BuildSDKJob(AsyncWorkerJob):
    def __init__(self, project):
        AsyncWorkerJob.__init__(self, project)

    def enqueue(self, queue, db):
        db.set_busy(self.project.builddir,
                    ["empty_project", "needs_build", "has_changes",
                     "build_done", "build_failed"])
        logging.info("Enqueueing project for building SDK")
        AsyncWorkerJob.enqueue(self, queue, db)

    def execute(self, db):
        success = self.build_failed
        try:
            logging.info("Build SDK started")
            self.project.build_sdk()
        # pylint: disable=broad-except
        except Exception:
            logging.exception("Build SDK Failed")
        else:
            if read_maxlevel(self.project.builddir) >= logging.ERROR:
                logging.info("Build finished with Error")
            else:
                logging.info("Build finished successfully")
                success = self.build_done
        finally:
            db.update_project_files(self.project)
            db.reset_busy(self.project.builddir, success) class BuildCDROMsJob(AsyncWorkerJob):
    def __init__(self, project, build_bin, build_src):
        AsyncWorkerJob.__init__(self, project)
        self.build_bin = build_bin
        self.build_src = build_src

    def enqueue(self, queue, db):
        db.set_busy(self.project.builddir,
                    ["empty_project", "needs_build", "has_changes",
                     "build_done", "build_failed"])
        logging.info("Enqueueing project for building CDROMs")
        AsyncWorkerJob.enqueue(self, queue, db)

    def execute(self, db):
        success = self.build_failed
        try:
            logging.info("Build CDROMs started")
            self.project.build_cdroms(self.build_bin, self.build_src)
        # pylint: disable=broad-except
        except Exception:
            logging.exception("Build CDROMs failed")
        else:
            if read_maxlevel(self.project.builddir) >= logging.ERROR:
                logging.info("Build finished with Error")
            else:
                logging.info("Build finished successfully")
                success = self.build_done
        finally:
            db.update_project_files(self.project)
            db.reset_busy(self.project.builddir, success) class ValidationMode:
    NO_CHECK = 1
    CHECK_BINARIES = 2
    CHECK_ALL = 0 def read_loggingQ(proj):
    return QHandler.pop(proj)
import errno
import os
from os import path
from threading import Lock
from uuid import uuid4
from shutil import rmtree
from elbepack.db import ElbeDB, get_versioned_filename
from elbepack.asyncworker import (AsyncWorker, BuildJob, APTUpdateJob,
                                  APTCommitJob, GenUpdateJob,
                                  SaveVersionJob, CheckoutVersionJob,
                                  APTUpdUpgrJob, BuildSysrootJob,
                                  PdebuildJob, CreatePbuilderJob,
                                  UpdatePbuilderJob, BuildChrootTarJob,
                                  BuildSDKJob, BuildCDROMsJob)
from elbepack.elbexml import ValidationMode
from elbepack.log import read_loggingQ
# ELBE - Debian Based Embedded Rootfilesystem Builder
# Copyright (c) 2014-2016 Torben Hohn <torben.hohn@linutronix.de>
# Copyright (c) 2014 Stefan Gast <stefan.gast@linutronix.de>
# Copyright (c) 2014-2018 Manuel Traut <manut@linutronix.de>
# Copyright (c) 2017 Philipp Arras <philipp.arras@linutronix.de>
#
# SPDX-License-Identifier: GPL-3.0-or-later







class ProjectManagerError(Exception):
    def __init__(self, message):
        Exception.__init__(self, message)


class AlreadyOpen(ProjectManagerError):
    def __init__(self, builddir, username):
        ProjectManagerError.__init__(
            self, "project in %s is already opened by %s" %
            (builddir, username))


class PermissionDenied(ProjectManagerError):
    def __init__(self, builddir):
        ProjectManagerError.__init__(
            self, "permission denied for project in %s" %
            builddir)


class NoOpenProject(ProjectManagerError):
    def __init__(self):
        ProjectManagerError.__init__(self, "must open a project first")


class InvalidState(ProjectManagerError):
    def __init__(self, message):
        ProjectManagerError.__init__(self, message)

class OpenProjectFile:
    def __init__(self, pfd, mode='r'):
        self.path = path.join(pfd.builddir, pfd.name)
        self.mime_type = pfd.mime_type
        self.fobj = open(self.path, mode)

class ProjectManager:

    # pylint: disable=too-many-public-methods

    def __init__(self, basepath):
        self.basepath = basepath    # Base path for new projects
        self.db = ElbeDB()          # Database of projects and users

        self.worker = AsyncWorker(self.db)