Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File structure checks #84

Merged
merged 21 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f01a03d
chore: startup of file structure checks
tyboro2002 Mar 7, 2024
b75c47b
chore: fxed get structure
tyboro2002 Mar 7, 2024
620b906
chore: fixed reading zip files
tyboro2002 Mar 7, 2024
72a0ee2
chore: one link for checks in project
BramMeir Mar 7, 2024
196e195
chore: added retrieving of structure checks for project
tyboro2002 Mar 7, 2024
3a74edb
Merge branch 'file_structure_checks' of https://github.com/SELab-2/UG…
tyboro2002 Mar 7, 2024
b1dbb18
fix: update tests
BramMeir Mar 7, 2024
aee9ad8
chore: finalized initialisation of file checks and added data
tyboro2002 Mar 7, 2024
082d497
Merge branch 'file_structure_checks' of https://github.com/SELab-2/UG…
tyboro2002 Mar 7, 2024
20f6ad5
chore: add i18n for errors
tyboro2002 Mar 7, 2024
ce077ce
chore: cleanup and fixed linting warnings
tyboro2002 Mar 7, 2024
83763ce
chore: cleanup the data directory and started tests
tyboro2002 Mar 8, 2024
998c51d
chore: worked at testing the file structure
tyboro2002 Mar 8, 2024
3da961a
chore: add some tests for filestructures
tyboro2002 Mar 9, 2024
df77e10
chore: finished file_structure tests
tyboro2002 Mar 9, 2024
27b9b82
Merge branch 'development' of https://github.com/SELab-2/UGent-7 into…
tyboro2002 Mar 9, 2024
cc0d8e2
chore: fix doable lintning warnings
tyboro2002 Mar 9, 2024
b1b20f2
chore: fixed some more linting errors
tyboro2002 Mar 9, 2024
a5984ba
chore: finaly fixed linter warnings
tyboro2002 Mar 9, 2024
0d4c015
chore: linting
EwoutV Mar 9, 2024
16e2e10
chore: linting
EwoutV Mar 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/.flake8
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Ignore unused imports
ignore = F401

max-line-length = 119
max-line-length = 120

max-complexity = 10

Expand Down
12 changes: 0 additions & 12 deletions backend/api/fixtures/checks.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,3 @@
- model: api.structurecheck
pk: 1
fields:
name: '.'
project: 123456
obligated_extensions:
- 3
- 4
blocked_extensions:
- 1
- 2

- model: api.extracheck
pk: 1
fields:
Expand Down
Empty file added backend/api/helpers/__init__.py
Empty file.
194 changes: 194 additions & 0 deletions backend/api/helpers/check_folder_structure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import zipfile
import os
from api.models.checks import StructureCheck
from api.models.extension import FileExtension
from django.utils.translation import gettext
from django.conf import settings


def parse_zip_file(project, dir_path): # TODO block paths that start with ..
dir_path = os.path.normpath(os.path.join(settings.MEDIA_ROOT, dir_path))
struct = get_zip_structure(dir_path)

sorted_keys = sorted(struct.keys())

for key in sorted_keys:
value = struct[key]
check = StructureCheck.objects.create(
name=key,
project=project
)
for ext in value["obligated_extensions"]:
extensie, _ = FileExtension.objects.get_or_create(
extension=ext
)
check.obligated_extensions.add(extensie.id)
project.structure_checks.add(check)


# TODO block paths that start with ..
def check_zip_file(project, dir_path, restrict_extra_folders=False):
dir_path = os.path.normpath(os.path.join(settings.MEDIA_ROOT, dir_path))
project_structure_checks = StructureCheck.objects.filter(project=project.id)
structuur = {}
for struct in project_structure_checks:
structuur[struct.name] = {
'obligated_extensions': set(),
'blocked_extensions': set()
}
for ext in struct.obligated_extensions.all():
structuur[struct.name]["obligated_extensions"].add(ext.extension)
for ext in struct.blocked_extensions.all():
structuur[struct.name]["blocked_extensions"].add(ext.extension)
return check_zip_structure(
structuur, dir_path, restrict_extra_folders=restrict_extra_folders)


def get_parent_directories(dir_path):
components = dir_path.split('/')
parents = set()
current_path = ""
for component in components:
if current_path != "":
current_path = current_path + "/" + component
else:
current_path = component
parents.add(current_path)
return parents


def list_zip_directories(zip_file_path):
"""
List all directories in a zip file.
:param zip_file_path: Path where the zip file is located.
:return: List of directory names.
"""
dirs = set()
with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
info_list = zip_ref.infolist()
for file in info_list:
if file.is_dir():
dir_path = file.filename[:-1]
parent_dirs = get_parent_directories(dir_path)
dirs.update(parent_dirs)
return dirs


def get_zip_structure(root_path):
directory_structure = {}
base, _ = os.path.splitext(root_path)
inhoud = list_zip_directories(root_path)
inhoud.add(".")
for inh in inhoud:
directory_structure[inh] = {
'obligated_extensions': set(),
'blocked_extensions': set()
}
with zipfile.ZipFile(root_path, 'r') as zip_file:
file_names = zip_file.namelist()
for file_name in file_names:
parts = file_name.rsplit('/', 1)
if len(parts) == 2:
map, file = parts
_, file_extension = os.path.splitext(file)
file_extension = file_extension[1:]
if not file_extension == "":
directory_structure[map]["obligated_extensions"].add(file_extension)
else:
_, file_extension = os.path.splitext(file_name)
file_extension = file_extension[1:]
directory_structure["."]["obligated_extensions"].add(file_extension)
return directory_structure


def check_zip_content(
root_path,
dir_path,
obligated_extensions,
blocked_extensions):
"""
Check the content of a directory without traversing subdirectories.
parameters:
dir_path: The path of the zip we need to check.
obligated_extensions: The file extensions that are obligated to be present.
blocked_extensions: The file extensions that are forbidden to be present.
:return:
True: All checks pass.
False: At least 1 blocked extension is found
or 1 obligated extension is not found.
"""
dir_path = dir_path.replace('\\', '/')
with zipfile.ZipFile(root_path, 'r') as zip_file:
zip_contents = set(zip_file.namelist())
found_obligated = set() # To track found obligated extensions
if dir_path == ".":
files_in_subdirectory = [file for file in zip_contents if "/" not in file]
else:
files_in_subdirectory = [
file[len(dir_path) + 1:] for file in zip_contents
if file.startswith(dir_path) and '/' not in file[len(dir_path) + 1:] and bool(file[len(dir_path) + 1:])]

for file in files_in_subdirectory:
_, file_extension = os.path.splitext(file)
# file_extension[1:] to remove the .
file_extension = file_extension[1:]

if file_extension in blocked_extensions:
# print(
# f"Error: {file_extension} found in
# '{dir_path}' is not allowed.") TODO
return False, gettext(
'zip.errors.invalid_structure.blocked_extension_found')
elif file_extension in obligated_extensions:
found_obligated.add(file_extension)
if set(obligated_extensions) <= found_obligated:
return True, gettext('zip.success')
else:
return False, gettext(
'zip.errors.invalid_structure.obligated_extension_not_found')


def check_zip_structure(
folder_structure,
zip_file_path,
restrict_extra_folders=False):
"""
Check the structure of a zip file.

parameters:
zip_file_path: Path to the zip file.
folder_structure: Dictionary representing the expected folder structure.
:return:
True: Zip file structure matches the expected structure.
False: Zip file structure does not match the expected structure.
"""
base, _ = os.path.splitext(zip_file_path)
struc = [f for f in folder_structure.keys() if not f == "."]

dirs = list_zip_directories(zip_file_path)
for dir in struc:
if dir not in dirs:
# print(f"Error: Directory '{dir}' not defined.") TODO
return False, gettext(
'zip.errors.invalid_structure.directory_not_defined')

for directory, info in folder_structure.items():
obligated_extensions = info.get('obligated_extensions', set())
blocked_extensions = info.get('blocked_extensions', set())

result, message = check_zip_content(
zip_file_path,
directory,
obligated_extensions,
blocked_extensions)
if not result:
return result, message
# Check for any directories not present in the folder structure dictionary
if restrict_extra_folders:
for actual_directory in dirs:
if actual_directory not in struc:
# print(f"Error: Directory '{actual_directory}'
# not defined in the folder structure dictionary.") TODO
return False, gettext(
'zip.errors.invalid_structure.directory_not_found_in_template')
return True, gettext('zip.success')
3 changes: 1 addition & 2 deletions backend/api/models/project.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from datetime import datetime
from django.db import models
from django.utils import timezone
from api.models.course import Course
Expand All @@ -21,7 +20,7 @@ class Project(models.Model):

start_date = models.DateTimeField(
# The default value is the current date and time
default=datetime.now,
default=timezone.now,
blank=True,
)

Expand Down
10 changes: 6 additions & 4 deletions backend/api/serializers/project_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ class ProjectSerializer(serializers.ModelSerializer):
many=False, read_only=True, view_name="course-detail"
)

structure_checks = serializers.HyperlinkedRelatedField(
many=True, read_only=True, view_name="structure-check-detail"
structure_checks = serializers.HyperlinkedIdentityField(
view_name="project-structure-checks",
read_only=True
)

extra_checks = serializers.HyperlinkedRelatedField(
many=True, read_only=True, view_name="extra-check-detail"
extra_checks = serializers.HyperlinkedIdentityField(
view_name="project-extra-checks",
read_only=True
)

groups = serializers.HyperlinkedIdentityField(
Expand Down
3 changes: 2 additions & 1 deletion backend/api/tests/test_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from api.models.extension import FileExtension
from api.models.project import Project
from api.models.course import Course
from django.conf import settings


def create_fileExtension(id, extension):
Expand Down Expand Up @@ -300,4 +301,4 @@ def test_extra_checks_exists(self):
# Assert the details of the retrieved Checks match the created Checks
retrieved_checks = content_json[0]
self.assertEqual(int(retrieved_checks["id"]), checks.id)
self.assertEqual(retrieved_checks["run_script"], "http://testserver" + checks.run_script.url)
self.assertEqual(retrieved_checks["run_script"], settings.TESTING_BASE_LINK + checks.run_script.url)
Loading
Loading