Pylint updates
This commit is contained in:
@@ -25608,4 +25608,3 @@ msgstr "Progresso"
|
|||||||
|
|
||||||
#~ msgid "individual"
|
#~ msgid "individual"
|
||||||
#~ msgstr "Creation"
|
#~ msgstr "Creation"
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
default_app_config = 'horilla_backup.apps.backupConfig'
|
default_app_config = "horilla_backup.apps.backupConfig"
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
|
||||||
admin.site.register(LocalBackup)
|
admin.site.register(LocalBackup)
|
||||||
admin.site.register(GoogleDriveBackup)
|
admin.site.register(GoogleDriveBackup)
|
||||||
|
|
||||||
|
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class BackupConfig(AppConfig):
|
class BackupConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
name = 'horilla_backup'
|
name = "horilla_backup"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
from horilla.urls import urlpatterns
|
from horilla.urls import urlpatterns
|
||||||
|
|
||||||
urlpatterns.append(
|
urlpatterns.append(
|
||||||
path("backup/", include("horilla_backup.urls")),
|
path("backup/", include("horilla_backup.urls")),
|
||||||
)
|
)
|
||||||
super().ready()
|
super().ready()
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,39 @@
|
|||||||
from django import forms
|
|
||||||
from .models import *
|
|
||||||
from base.forms import ModelForm
|
|
||||||
from django.template.loader import render_to_string
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from pathlib import Path
|
|
||||||
from .gdrive import authenticate
|
|
||||||
from django.core.files.storage import default_storage
|
|
||||||
from django.core.files.base import ContentFile
|
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from base.forms import ModelForm
|
||||||
|
|
||||||
|
from .gdrive import authenticate
|
||||||
|
from .models import *
|
||||||
|
|
||||||
|
|
||||||
class LocalBackupSetupForm(ModelForm):
|
class LocalBackupSetupForm(ModelForm):
|
||||||
verbose_name = "Server Backup"
|
verbose_name = "Server Backup"
|
||||||
backup_db = forms.BooleanField(required=False, help_text="Enable to backup database to server.")
|
backup_db = forms.BooleanField(
|
||||||
backup_media = forms.BooleanField(required=False, help_text="Enable to backup all media files to server.")
|
required=False, help_text="Enable to backup database to server."
|
||||||
interval = forms.BooleanField(required=False, help_text="Enable to automate the backup in a period of seconds.")
|
)
|
||||||
fixed = forms.BooleanField(required=False, help_text="Enable to automate the backup in a fixed time.")
|
backup_media = forms.BooleanField(
|
||||||
|
required=False, help_text="Enable to backup all media files to server."
|
||||||
|
)
|
||||||
|
interval = forms.BooleanField(
|
||||||
|
required=False,
|
||||||
|
help_text="Enable to automate the backup in a period of seconds.",
|
||||||
|
)
|
||||||
|
fixed = forms.BooleanField(
|
||||||
|
required=False, help_text="Enable to automate the backup in a fixed time."
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = LocalBackup
|
model = LocalBackup
|
||||||
exclude = ['active']
|
exclude = ["active"]
|
||||||
|
|
||||||
|
|
||||||
def as_p(self):
|
def as_p(self):
|
||||||
"""
|
"""
|
||||||
Render the form fields as HTML table rows with Bootstrap styling.
|
Render the form fields as HTML table rows with Bootstrap styling.
|
||||||
@@ -30,64 +41,65 @@ class LocalBackupSetupForm(ModelForm):
|
|||||||
context = {"form": self}
|
context = {"form": self}
|
||||||
table_html = render_to_string("common_form.html", context)
|
table_html = render_to_string("common_form.html", context)
|
||||||
return table_html
|
return table_html
|
||||||
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
backup_db = cleaned_data.get('backup_db')
|
backup_db = cleaned_data.get("backup_db")
|
||||||
backup_media = cleaned_data.get('backup_media')
|
backup_media = cleaned_data.get("backup_media")
|
||||||
interval = cleaned_data.get('interval')
|
interval = cleaned_data.get("interval")
|
||||||
fixed = cleaned_data.get('fixed')
|
fixed = cleaned_data.get("fixed")
|
||||||
seconds = cleaned_data.get('seconds')
|
seconds = cleaned_data.get("seconds")
|
||||||
hour = cleaned_data.get('hour')
|
hour = cleaned_data.get("hour")
|
||||||
minute = cleaned_data.get('minute')
|
minute = cleaned_data.get("minute")
|
||||||
backup_path = cleaned_data.get('backup_path')
|
backup_path = cleaned_data.get("backup_path")
|
||||||
path = Path(backup_path)
|
path = Path(backup_path)
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
raise ValidationError({
|
raise ValidationError({"backup_path": _("The directory does not exist.")})
|
||||||
'backup_path': _('The directory does not exist.')
|
|
||||||
})
|
|
||||||
if backup_db == False and backup_media == False:
|
if backup_db == False and backup_media == False:
|
||||||
raise forms.ValidationError("Please select any backup option.")
|
raise forms.ValidationError("Please select any backup option.")
|
||||||
if interval == False and fixed == False:
|
if interval == False and fixed == False:
|
||||||
raise forms.ValidationError("Please select any backup automate option.")
|
raise forms.ValidationError("Please select any backup automate option.")
|
||||||
if interval == True and seconds == None:
|
if interval == True and seconds == None:
|
||||||
raise ValidationError({
|
raise ValidationError({"seconds": _("This field is required.")})
|
||||||
'seconds': _('This field is required.')
|
if fixed == True and hour == None:
|
||||||
})
|
raise ValidationError({"hour": _("This field is required.")})
|
||||||
if fixed == True and hour == None:
|
|
||||||
raise ValidationError({
|
|
||||||
'hour': _('This field is required.')
|
|
||||||
})
|
|
||||||
if seconds:
|
if seconds:
|
||||||
if seconds < 0:
|
if seconds < 0:
|
||||||
raise ValidationError({
|
raise ValidationError(
|
||||||
'seconds': _('Negative value is not accepatable.')
|
{"seconds": _("Negative value is not accepatable.")}
|
||||||
})
|
)
|
||||||
if hour:
|
if hour:
|
||||||
if hour < 0 or hour > 24:
|
if hour < 0 or hour > 24:
|
||||||
raise ValidationError({
|
raise ValidationError({"hour": _("Enter a hour between 0 to 24.")})
|
||||||
'hour': _('Enter a hour between 0 to 24.')
|
if minute:
|
||||||
})
|
|
||||||
if minute:
|
|
||||||
if minute < 0 or minute > 60:
|
if minute < 0 or minute > 60:
|
||||||
raise ValidationError({
|
raise ValidationError({"minute": _("Enter a minute between 0 to 60.")})
|
||||||
'minute': _('Enter a minute between 0 to 60.')
|
return cleaned_data
|
||||||
})
|
|
||||||
return cleaned_data
|
|
||||||
|
|
||||||
|
|
||||||
class GdriveBackupSetupForm(ModelForm):
|
class GdriveBackupSetupForm(ModelForm):
|
||||||
verbose_name = "Gdrive Backup"
|
verbose_name = "Gdrive Backup"
|
||||||
backup_db = forms.BooleanField(required=False, label="Backup DB", help_text="Enable to backup database to Gdrive")
|
backup_db = forms.BooleanField(
|
||||||
backup_media = forms.BooleanField(required=False, label="Backup Media", help_text="Enable to backup all media files to Gdrive")
|
required=False,
|
||||||
interval = forms.BooleanField(required=False, help_text="Enable to automate the backup in a period of seconds.")
|
label="Backup DB",
|
||||||
fixed = forms.BooleanField(required=False, help_text="Enable to automate the backup in a fixed time.")
|
help_text="Enable to backup database to Gdrive",
|
||||||
|
)
|
||||||
|
backup_media = forms.BooleanField(
|
||||||
|
required=False,
|
||||||
|
label="Backup Media",
|
||||||
|
help_text="Enable to backup all media files to Gdrive",
|
||||||
|
)
|
||||||
|
interval = forms.BooleanField(
|
||||||
|
required=False,
|
||||||
|
help_text="Enable to automate the backup in a period of seconds.",
|
||||||
|
)
|
||||||
|
fixed = forms.BooleanField(
|
||||||
|
required=False, help_text="Enable to automate the backup in a fixed time."
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GoogleDriveBackup
|
model = GoogleDriveBackup
|
||||||
exclude = ['active']
|
exclude = ["active"]
|
||||||
|
|
||||||
|
|
||||||
def as_p(self):
|
def as_p(self):
|
||||||
"""
|
"""
|
||||||
@@ -96,18 +108,18 @@ class GdriveBackupSetupForm(ModelForm):
|
|||||||
context = {"form": self}
|
context = {"form": self}
|
||||||
table_html = render_to_string("common_form.html", context)
|
table_html = render_to_string("common_form.html", context)
|
||||||
return table_html
|
return table_html
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
backup_db = cleaned_data.get('backup_db')
|
backup_db = cleaned_data.get("backup_db")
|
||||||
backup_media = cleaned_data.get('backup_media')
|
backup_media = cleaned_data.get("backup_media")
|
||||||
interval = cleaned_data.get('interval')
|
interval = cleaned_data.get("interval")
|
||||||
fixed = cleaned_data.get('fixed')
|
fixed = cleaned_data.get("fixed")
|
||||||
seconds = cleaned_data.get('seconds')
|
seconds = cleaned_data.get("seconds")
|
||||||
hour = cleaned_data.get('hour')
|
hour = cleaned_data.get("hour")
|
||||||
minute = cleaned_data.get('minute')
|
minute = cleaned_data.get("minute")
|
||||||
service_account_file = cleaned_data.get('service_account_file')
|
service_account_file = cleaned_data.get("service_account_file")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if GoogleDriveBackup.objects.exists():
|
if GoogleDriveBackup.objects.exists():
|
||||||
authenticate(service_account_file.path)
|
authenticate(service_account_file.path)
|
||||||
@@ -115,41 +127,35 @@ class GdriveBackupSetupForm(ModelForm):
|
|||||||
file_data = service_account_file.read()
|
file_data = service_account_file.read()
|
||||||
# Save the processed file to the desired location
|
# Save the processed file to the desired location
|
||||||
file_name = service_account_file.name
|
file_name = service_account_file.name
|
||||||
new_file_name = file_name
|
new_file_name = file_name
|
||||||
# Save using Django's default storage system
|
# Save using Django's default storage system
|
||||||
relative_path = default_storage.save(new_file_name, ContentFile(file_data))
|
relative_path = default_storage.save(
|
||||||
|
new_file_name, ContentFile(file_data)
|
||||||
|
)
|
||||||
# Get the full absolute path
|
# Get the full absolute path
|
||||||
full_path = default_storage.path(relative_path)
|
full_path = default_storage.path(relative_path)
|
||||||
authenticate(full_path)
|
authenticate(full_path)
|
||||||
os.remove(full_path)
|
os.remove(full_path)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise forms.ValidationError("Please provide a valid service account file.")
|
raise forms.ValidationError("Please provide a valid service account file.")
|
||||||
if backup_db == False and backup_media == False:
|
if backup_db == False and backup_media == False:
|
||||||
raise forms.ValidationError("Please select any backup option.")
|
raise forms.ValidationError("Please select any backup option.")
|
||||||
if interval == False and fixed == False:
|
if interval == False and fixed == False:
|
||||||
raise forms.ValidationError("Please select any backup automate option.")
|
raise forms.ValidationError("Please select any backup automate option.")
|
||||||
if interval == True and seconds == None:
|
if interval == True and seconds == None:
|
||||||
raise ValidationError({
|
raise ValidationError({"seconds": _("This field is required.")})
|
||||||
'seconds': _('This field is required.')
|
if fixed == True and hour == None:
|
||||||
})
|
raise ValidationError({"hour": _("This field is required.")})
|
||||||
if fixed == True and hour == None:
|
|
||||||
raise ValidationError({
|
|
||||||
'hour': _('This field is required.')
|
|
||||||
})
|
|
||||||
if seconds:
|
if seconds:
|
||||||
if seconds < 0:
|
if seconds < 0:
|
||||||
raise ValidationError({
|
raise ValidationError(
|
||||||
'seconds': _('Negative value is not accepatable.')
|
{"seconds": _("Negative value is not accepatable.")}
|
||||||
})
|
)
|
||||||
if hour:
|
if hour:
|
||||||
if hour < 0 or hour > 24:
|
if hour < 0 or hour > 24:
|
||||||
raise ValidationError({
|
raise ValidationError({"hour": _("Enter a hour between 0 to 24.")})
|
||||||
'hour': _('Enter a hour between 0 to 24.')
|
if minute:
|
||||||
})
|
|
||||||
if minute:
|
|
||||||
if minute < 0 or minute > 60:
|
if minute < 0 or minute > 60:
|
||||||
raise ValidationError({
|
raise ValidationError({"minute": _("Enter a minute between 0 to 60.")})
|
||||||
'minute': _('Enter a minute between 0 to 60.')
|
return cleaned_data
|
||||||
})
|
|
||||||
return cleaned_data
|
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
from googleapiclient.discovery import build
|
|
||||||
from google.oauth2 import service_account
|
|
||||||
from googleapiclient.http import MediaFileUpload
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from google.oauth2 import service_account
|
||||||
SCOPES = ['https://www.googleapis.com/auth/drive']
|
from googleapiclient.discovery import build
|
||||||
|
from googleapiclient.http import MediaFileUpload
|
||||||
|
|
||||||
|
SCOPES = ["https://www.googleapis.com/auth/drive"]
|
||||||
|
|
||||||
|
|
||||||
def authenticate(service_account_file):
|
def authenticate(service_account_file):
|
||||||
creds = service_account.Credentials.from_service_account_file(service_account_file, scopes=SCOPES)
|
creds = service_account.Credentials.from_service_account_file(
|
||||||
|
service_account_file, scopes=SCOPES
|
||||||
|
)
|
||||||
return creds
|
return creds
|
||||||
|
|
||||||
|
|
||||||
def upload_file(file_path, service_account_file, parent_folder_id):
|
def upload_file(file_path, service_account_file, parent_folder_id):
|
||||||
creds = authenticate(service_account_file)
|
creds = authenticate(service_account_file)
|
||||||
service = build('drive', 'v3', credentials=creds)
|
service = build("drive", "v3", credentials=creds)
|
||||||
parent_folder_id = parent_folder_id
|
parent_folder_id = parent_folder_id
|
||||||
|
|
||||||
file_metadata = {
|
file_metadata = {"name": os.path.basename(file_path), "parents": [parent_folder_id]}
|
||||||
'name' : os.path.basename(file_path),
|
|
||||||
'parents' : [parent_folder_id]
|
|
||||||
}
|
|
||||||
media = MediaFileUpload(file_path, resumable=True)
|
media = MediaFileUpload(file_path, resumable=True)
|
||||||
file = service.files().create(
|
file = (
|
||||||
body=file_metadata,
|
service.files()
|
||||||
media_body=media,
|
.create(body=file_metadata, media_body=media, fields="id")
|
||||||
fields='id'
|
.execute()
|
||||||
).execute()
|
)
|
||||||
|
|||||||
@@ -3,16 +3,18 @@ import atexit
|
|||||||
|
|
||||||
def shutdown_function():
|
def shutdown_function():
|
||||||
from horilla_backup.models import GoogleDriveBackup, LocalBackup
|
from horilla_backup.models import GoogleDriveBackup, LocalBackup
|
||||||
|
|
||||||
if GoogleDriveBackup.objects.exists():
|
if GoogleDriveBackup.objects.exists():
|
||||||
google_drive_backup = GoogleDriveBackup.objects.first()
|
google_drive_backup = GoogleDriveBackup.objects.first()
|
||||||
google_drive_backup.active = False
|
google_drive_backup.active = False
|
||||||
google_drive_backup.save()
|
google_drive_backup.save()
|
||||||
if LocalBackup.objects.exists():
|
if LocalBackup.objects.exists():
|
||||||
local_backup = LocalBackup.objects.first()
|
local_backup = LocalBackup.objects.first()
|
||||||
local_backup.active = False
|
local_backup.active = False
|
||||||
local_backup.save()
|
local_backup.save()
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
atexit.register(shutdown_function)
|
atexit.register(shutdown_function)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
#Create your models here.
|
# Create your models here.
|
||||||
|
|
||||||
|
|
||||||
class LocalBackup(models.Model):
|
class LocalBackup(models.Model):
|
||||||
backup_path = models.CharField(max_length=255, help_text="Specify the path in the server were the backup files should keep")
|
backup_path = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
help_text="Specify the path in the server were the backup files should keep",
|
||||||
|
)
|
||||||
backup_media = models.BooleanField(blank=True, null=True)
|
backup_media = models.BooleanField(blank=True, null=True)
|
||||||
backup_db = models.BooleanField(blank=True, null=True)
|
backup_db = models.BooleanField(blank=True, null=True)
|
||||||
interval = models.BooleanField(blank=True, null=True)
|
interval = models.BooleanField(blank=True, null=True)
|
||||||
fixed = models.BooleanField(blank=True, null=True)
|
fixed = models.BooleanField(blank=True, null=True)
|
||||||
seconds = models.IntegerField(blank=True, null=True)
|
seconds = models.IntegerField(blank=True, null=True)
|
||||||
hour = models.IntegerField(blank=True, null=True)
|
hour = models.IntegerField(blank=True, null=True)
|
||||||
minute = models.IntegerField(blank=True, null=True)
|
minute = models.IntegerField(blank=True, null=True)
|
||||||
active = models.BooleanField(default=False)
|
active = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
# Check if there's an existing instance
|
# Check if there's an existing instance
|
||||||
if self.interval == False:
|
if self.interval == False:
|
||||||
self.seconds = None
|
self.seconds = None
|
||||||
if self.fixed == False:
|
if self.fixed == False:
|
||||||
self.hour = None
|
self.hour = None
|
||||||
self.minute = None
|
self.minute = None
|
||||||
if LocalBackup.objects.exists():
|
if LocalBackup.objects.exists():
|
||||||
@@ -26,7 +29,7 @@ class LocalBackup(models.Model):
|
|||||||
existing_instance = LocalBackup.objects.first()
|
existing_instance = LocalBackup.objects.first()
|
||||||
# Update the fields of the existing instance with the new data
|
# Update the fields of the existing instance with the new data
|
||||||
for field in self._meta.fields:
|
for field in self._meta.fields:
|
||||||
if field.name != 'id': # Avoid changing the primary key
|
if field.name != "id": # Avoid changing the primary key
|
||||||
setattr(existing_instance, field.name, getattr(self, field.name))
|
setattr(existing_instance, field.name, getattr(self, field.name))
|
||||||
# Save the updated instance
|
# Save the updated instance
|
||||||
super(LocalBackup, existing_instance).save(*args, **kwargs)
|
super(LocalBackup, existing_instance).save(*args, **kwargs)
|
||||||
@@ -38,22 +41,25 @@ class LocalBackup(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class GoogleDriveBackup(models.Model):
|
class GoogleDriveBackup(models.Model):
|
||||||
service_account_file = models.FileField(upload_to="gdrive_service_account_file",
|
service_account_file = models.FileField(
|
||||||
verbose_name="Service Account File",
|
upload_to="gdrive_service_account_file",
|
||||||
help_text="Make sure your file is in JSON format and contains your Google Service Account credentials")
|
verbose_name="Service Account File",
|
||||||
gdrive_folder_id = models.CharField(max_length=255,
|
help_text="Make sure your file is in JSON format and contains your Google Service Account credentials",
|
||||||
verbose_name="Gdrive Folder ID",
|
)
|
||||||
help_text="Shared Gdrive folder Id with access granted to Gmail service account. Enable full permissions for seamless connection.")
|
gdrive_folder_id = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
verbose_name="Gdrive Folder ID",
|
||||||
|
help_text="Shared Gdrive folder Id with access granted to Gmail service account. Enable full permissions for seamless connection.",
|
||||||
|
)
|
||||||
backup_media = models.BooleanField(blank=True, null=True)
|
backup_media = models.BooleanField(blank=True, null=True)
|
||||||
backup_db = models.BooleanField(blank=True, null=True)
|
backup_db = models.BooleanField(blank=True, null=True)
|
||||||
interval = models.BooleanField(blank=True, null=True)
|
interval = models.BooleanField(blank=True, null=True)
|
||||||
fixed = models.BooleanField(blank=True, null=True)
|
fixed = models.BooleanField(blank=True, null=True)
|
||||||
seconds = models.IntegerField(blank=True, null=True)
|
seconds = models.IntegerField(blank=True, null=True)
|
||||||
hour = models.IntegerField(blank=True, null=True)
|
hour = models.IntegerField(blank=True, null=True)
|
||||||
minute = models.IntegerField(blank=True, null=True)
|
minute = models.IntegerField(blank=True, null=True)
|
||||||
active = models.BooleanField(default=False)
|
active = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
# Check if there's an existing instance
|
# Check if there's an existing instance
|
||||||
if self.interval == False:
|
if self.interval == False:
|
||||||
@@ -66,7 +72,7 @@ class GoogleDriveBackup(models.Model):
|
|||||||
existing_instance = GoogleDriveBackup.objects.first()
|
existing_instance = GoogleDriveBackup.objects.first()
|
||||||
# Update the fields of the existing instance with the new data
|
# Update the fields of the existing instance with the new data
|
||||||
for field in self._meta.fields:
|
for field in self._meta.fields:
|
||||||
if field.name != 'id': # Avoid changing the primary key
|
if field.name != "id": # Avoid changing the primary key
|
||||||
setattr(existing_instance, field.name, getattr(self, field.name))
|
setattr(existing_instance, field.name, getattr(self, field.name))
|
||||||
# Save the updated instance
|
# Save the updated instance
|
||||||
super(GoogleDriveBackup, existing_instance).save(*args, **kwargs)
|
super(GoogleDriveBackup, existing_instance).save(*args, **kwargs)
|
||||||
@@ -74,4 +80,4 @@ class GoogleDriveBackup(models.Model):
|
|||||||
else:
|
else:
|
||||||
# If no existing instance, proceed with regular save
|
# If no existing instance, proceed with regular save
|
||||||
super(GoogleDriveBackup, self).save(*args, **kwargs)
|
super(GoogleDriveBackup, self).save(*args, **kwargs)
|
||||||
return self
|
return self
|
||||||
|
|||||||
@@ -1,30 +1,38 @@
|
|||||||
import subprocess
|
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
def dump_postgres_db(db_name, username, output_file, password=None, host='localhost', port=5432):
|
|
||||||
|
def dump_postgres_db(
|
||||||
|
db_name, username, output_file, password=None, host="localhost", port=5432
|
||||||
|
):
|
||||||
# Set environment variable for the password if provided
|
# Set environment variable for the password if provided
|
||||||
if password:
|
if password:
|
||||||
os.environ['PGPASSWORD'] = password
|
os.environ["PGPASSWORD"] = password
|
||||||
|
|
||||||
# Construct the pg_dump command
|
# Construct the pg_dump command
|
||||||
dump_command = [
|
dump_command = [
|
||||||
'pg_dump',
|
"pg_dump",
|
||||||
'-h', host,
|
"-h",
|
||||||
'-p', str(port),
|
host,
|
||||||
'-U', username,
|
"-p",
|
||||||
'-F', 'c', # Custom format
|
str(port),
|
||||||
'-f', output_file,
|
"-U",
|
||||||
db_name
|
username,
|
||||||
|
"-F",
|
||||||
|
"c", # Custom format
|
||||||
|
"-f",
|
||||||
|
output_file,
|
||||||
|
db_name,
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Execute the pg_dump command
|
# Execute the pg_dump command
|
||||||
result = subprocess.run(dump_command, check=True, text=True, capture_output=True)
|
result = subprocess.run(
|
||||||
except subprocess.CalledProcessError as e:
|
dump_command, check=True, text=True, capture_output=True
|
||||||
pass
|
)
|
||||||
finally:
|
except subprocess.CalledProcessError as e:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
# Clean up the environment variable
|
# Clean up the environment variable
|
||||||
if password:
|
if password:
|
||||||
del os.environ['PGPASSWORD']
|
del os.environ["PGPASSWORD"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
import os
|
|
||||||
|
from horilla import settings
|
||||||
|
|
||||||
|
from .gdrive import *
|
||||||
|
|
||||||
# from horilla.settings import DBBACKUP_STORAGE_OPTIONS
|
# from horilla.settings import DBBACKUP_STORAGE_OPTIONS
|
||||||
from .models import *
|
from .models import *
|
||||||
from .gdrive import *
|
|
||||||
from .pgdump import *
|
from .pgdump import *
|
||||||
from horilla import settings
|
|
||||||
from .zip import *
|
from .zip import *
|
||||||
|
|
||||||
scheduler = BackgroundScheduler()
|
scheduler = BackgroundScheduler()
|
||||||
@@ -17,7 +21,7 @@ scheduler = BackgroundScheduler()
|
|||||||
# DBBACKUP_STORAGE_OPTIONS['location'] = local_backup.backup_path
|
# DBBACKUP_STORAGE_OPTIONS['location'] = local_backup.backup_path
|
||||||
# folder_path = DBBACKUP_STORAGE_OPTIONS['location']
|
# folder_path = DBBACKUP_STORAGE_OPTIONS['location']
|
||||||
# if local_backup.backup_db:
|
# if local_backup.backup_db:
|
||||||
# call_command('dbbackup')
|
# call_command('dbbackup')
|
||||||
# if local_backup.backup_media:
|
# if local_backup.backup_media:
|
||||||
# call_command("mediabackup")
|
# call_command("mediabackup")
|
||||||
# files = sorted(os.listdir(folder_path), key=lambda x: os.path.getctime(os.path.join(folder_path, x)))
|
# files = sorted(os.listdir(folder_path), key=lambda x: os.path.getctime(os.path.join(folder_path, x)))
|
||||||
@@ -32,7 +36,7 @@ scheduler = BackgroundScheduler()
|
|||||||
# except:
|
# except:
|
||||||
# pass
|
# pass
|
||||||
|
|
||||||
|
|
||||||
# def start_backup_job():
|
# def start_backup_job():
|
||||||
# """
|
# """
|
||||||
# Start the backup job based on the LocalBackup configuration.
|
# Start the backup job based on the LocalBackup configuration.
|
||||||
@@ -45,25 +49,25 @@ scheduler = BackgroundScheduler()
|
|||||||
# try:
|
# try:
|
||||||
# scheduler.remove_job('backup_job')
|
# scheduler.remove_job('backup_job')
|
||||||
# except:
|
# except:
|
||||||
# pass
|
# pass
|
||||||
|
|
||||||
# # Add new job based on LocalBackup configuration
|
# # Add new job based on LocalBackup configuration
|
||||||
# if local_backup.interval:
|
# if local_backup.interval:
|
||||||
# scheduler.add_job(backup_database, 'interval', seconds=local_backup.seconds, id='backup_job')
|
# scheduler.add_job(backup_database, 'interval', seconds=local_backup.seconds, id='backup_job')
|
||||||
# else:
|
# else:
|
||||||
# scheduler.add_job(backup_database, trigger='cron', hour=local_backup.hour, minute=local_backup.minute, id='backup_job')
|
# scheduler.add_job(backup_database, trigger='cron', hour=local_backup.hour, minute=local_backup.minute, id='backup_job')
|
||||||
# # Start the scheduler if it's not already running
|
# # Start the scheduler if it's not already running
|
||||||
# if not scheduler.running:
|
# if not scheduler.running:
|
||||||
# scheduler.start()
|
# scheduler.start()
|
||||||
# else:
|
# else:
|
||||||
# stop_backup_job()
|
# stop_backup_job()
|
||||||
|
|
||||||
|
|
||||||
# def stop_backup_job():
|
# def stop_backup_job():
|
||||||
# """
|
# """
|
||||||
# Stop the backup job if it exists.
|
# Stop the backup job if it exists.
|
||||||
# """
|
# """
|
||||||
# try:
|
# try:
|
||||||
# scheduler.remove_job('backup_job')
|
# scheduler.remove_job('backup_job')
|
||||||
# except:
|
# except:
|
||||||
# pass
|
# pass
|
||||||
@@ -83,19 +87,19 @@ def google_drive_backup():
|
|||||||
service_account_file = google_drive.service_account_file.path
|
service_account_file = google_drive.service_account_file.path
|
||||||
gdrive_folder_id = google_drive.gdrive_folder_id
|
gdrive_folder_id = google_drive.gdrive_folder_id
|
||||||
if google_drive.backup_db:
|
if google_drive.backup_db:
|
||||||
db = settings.DATABASES['default']
|
db = settings.DATABASES["default"]
|
||||||
dump_postgres_db(
|
dump_postgres_db(
|
||||||
db_name=db['NAME'],
|
db_name=db["NAME"],
|
||||||
username=db['USER'],
|
username=db["USER"],
|
||||||
output_file='backupdb.dump',
|
output_file="backupdb.dump",
|
||||||
password=db['PASSWORD']
|
password=db["PASSWORD"],
|
||||||
)
|
)
|
||||||
upload_file('backupdb.dump', service_account_file, gdrive_folder_id)
|
upload_file("backupdb.dump", service_account_file, gdrive_folder_id)
|
||||||
os.remove('backupdb.dump')
|
os.remove("backupdb.dump")
|
||||||
if google_drive.backup_media:
|
if google_drive.backup_media:
|
||||||
folder_to_zip = settings.MEDIA_ROOT
|
folder_to_zip = settings.MEDIA_ROOT
|
||||||
output_zip_file = "media.zip"
|
output_zip_file = "media.zip"
|
||||||
zip_folder(folder_to_zip, output_zip_file)
|
zip_folder(folder_to_zip, output_zip_file)
|
||||||
upload_file("media.zip", service_account_file, gdrive_folder_id)
|
upload_file("media.zip", service_account_file, gdrive_folder_id)
|
||||||
os.remove("media.zip")
|
os.remove("media.zip")
|
||||||
|
|
||||||
@@ -108,31 +112,42 @@ def start_gdrive_backup_job():
|
|||||||
if GoogleDriveBackup.objects.exists():
|
if GoogleDriveBackup.objects.exists():
|
||||||
gdrive_backup = GoogleDriveBackup.objects.first()
|
gdrive_backup = GoogleDriveBackup.objects.first()
|
||||||
|
|
||||||
# Remove existing job if it exists
|
# Remove existing job if it exists
|
||||||
try:
|
try:
|
||||||
scheduler.remove_job('backup_job')
|
scheduler.remove_job("backup_job")
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
# Add new job based on Gdrive Backup configuration
|
# Add new job based on Gdrive Backup configuration
|
||||||
if gdrive_backup.interval:
|
if gdrive_backup.interval:
|
||||||
scheduler.add_job(google_drive_backup, 'interval', seconds=gdrive_backup.seconds, id='gdrive_backup_job')
|
scheduler.add_job(
|
||||||
else:
|
google_drive_backup,
|
||||||
scheduler.add_job(google_drive_backup, trigger='cron', hour=gdrive_backup.hour, minute=gdrive_backup.minute, id='gdrive_backup_job')
|
"interval",
|
||||||
|
seconds=gdrive_backup.seconds,
|
||||||
|
id="gdrive_backup_job",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
scheduler.add_job(
|
||||||
|
google_drive_backup,
|
||||||
|
trigger="cron",
|
||||||
|
hour=gdrive_backup.hour,
|
||||||
|
minute=gdrive_backup.minute,
|
||||||
|
id="gdrive_backup_job",
|
||||||
|
)
|
||||||
|
|
||||||
# Start the scheduler if it's not already running
|
# Start the scheduler if it's not already running
|
||||||
if not scheduler.running:
|
if not scheduler.running:
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
stop_gdrive_backup_job()
|
stop_gdrive_backup_job()
|
||||||
|
|
||||||
|
|
||||||
def stop_gdrive_backup_job():
|
def stop_gdrive_backup_job():
|
||||||
"""
|
"""
|
||||||
Stop the backup job if it exists.
|
Stop the backup job if it exists.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
scheduler.remove_job('gdrive_backup_job')
|
scheduler.remove_job("gdrive_backup_job")
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -143,4 +158,3 @@ def stop_gdrive_backup_job():
|
|||||||
# """
|
# """
|
||||||
# stop_gdrive_backup_job()
|
# stop_gdrive_backup_job()
|
||||||
# start_gdrive_backup_job()
|
# start_gdrive_backup_job()
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
<img style="display: block; width: 15%; margin: 20px auto; filter: opacity(0.5);" src="/static/images/ui/server error.png" class="" alt="Page not found. 404.">
|
<img style="display: block; width: 15%; margin: 20px auto; filter: opacity(0.5);" src="/static/images/ui/server error.png" class="" alt="Page not found. 404.">
|
||||||
<h5 class="oh-404__subtitle">Only work with postgresql database.</h5>
|
<h5 class="oh-404__subtitle">Only work with postgresql database.</h5>
|
||||||
</div>
|
</div>
|
||||||
{% endblock settings %}
|
{% endblock settings %}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
$('.oh-payslip__header').append(`<div class="d-flex">
|
$('.oh-payslip__header').append(`<div class="d-flex">
|
||||||
{% if show %}
|
{% if show %}
|
||||||
{% if active %}
|
{% if active %}
|
||||||
@@ -39,9 +39,9 @@
|
|||||||
if (!$('#id_fixed').is(':checked')) {
|
if (!$('#id_fixed').is(':checked')) {
|
||||||
$('#id_hour').parent().hide();``
|
$('#id_hour').parent().hide();``
|
||||||
$('#id_minute').parent().hide();
|
$('#id_minute').parent().hide();
|
||||||
}
|
}
|
||||||
$('#id_interval').change(function() {
|
$('#id_interval').change(function() {
|
||||||
if ($(this).is(':checked')) {
|
if ($(this).is(':checked')) {
|
||||||
$('#id_fixed4').prop('checked', false);
|
$('#id_fixed4').prop('checked', false);
|
||||||
$('#id_hour').parent().hide();
|
$('#id_hour').parent().hide();
|
||||||
$('#id_minute').parent().hide();
|
$('#id_minute').parent().hide();
|
||||||
@@ -64,4 +64,4 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock settings %}
|
{% endblock settings %}
|
||||||
|
|||||||
@@ -65,4 +65,4 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock settings %}
|
{% endblock settings %}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import *
|
from .views import *
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -7,5 +8,5 @@ urlpatterns = [
|
|||||||
# path("delete/", local_Backup_delete, name="backup_delete"),
|
# path("delete/", local_Backup_delete, name="backup_delete"),
|
||||||
path("gdrive/", gdrive_setup, name="gdrive"),
|
path("gdrive/", gdrive_setup, name="gdrive"),
|
||||||
path("gdrive-start-stop/", gdrive_Backup_stop_or_start, name="gdrive_start_stop"),
|
path("gdrive-start-stop/", gdrive_Backup_stop_or_start, name="gdrive_start_stop"),
|
||||||
path("gdrive-delete/", gdrive_Backup_delete, name="gdrive_delete")
|
path("gdrive-delete/", gdrive_Backup_delete, name="gdrive_delete"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
from django.shortcuts import render, redirect
|
from django.contrib import messages
|
||||||
|
from django.db import connection
|
||||||
|
from django.shortcuts import redirect, render
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from horilla.decorators import (
|
from horilla.decorators import (
|
||||||
hx_request_required,
|
hx_request_required,
|
||||||
login_required,
|
login_required,
|
||||||
@@ -6,20 +10,17 @@ from horilla.decorators import (
|
|||||||
owner_can_enter,
|
owner_can_enter,
|
||||||
permission_required,
|
permission_required,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .forms import *
|
from .forms import *
|
||||||
from django.contrib import messages
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from .scheduler import *
|
|
||||||
from .gdrive import *
|
from .gdrive import *
|
||||||
from .pgdump import *
|
from .pgdump import *
|
||||||
|
from .scheduler import *
|
||||||
from .zip import *
|
from .zip import *
|
||||||
from django.db import connection
|
|
||||||
|
|
||||||
|
|
||||||
# @login_required
|
# @login_required
|
||||||
# @permission_required("backup.add_localbackup")
|
# @permission_required("backup.add_localbackup")
|
||||||
# def local_setup(request):
|
# def local_setup(request):
|
||||||
# """
|
# """
|
||||||
# function used to setup local backup.
|
# function used to setup local backup.
|
||||||
|
|
||||||
# Parameters:
|
# Parameters:
|
||||||
@@ -39,13 +40,13 @@ from django.db import connection
|
|||||||
# if request.method == "POST":
|
# if request.method == "POST":
|
||||||
# form = LocalBackupSetupForm(request.POST, request.FILES)
|
# form = LocalBackupSetupForm(request.POST, request.FILES)
|
||||||
# if form.is_valid():
|
# if form.is_valid():
|
||||||
# form.save()
|
# form.save()
|
||||||
# stop_backup_job()
|
# stop_backup_job()
|
||||||
# messages.success(request, _("Local backup automation setup updated."))
|
# messages.success(request, _("Local backup automation setup updated."))
|
||||||
# return redirect("local")
|
# return redirect("local")
|
||||||
# return render(request, "backup/local_setup_form.html", {"form": form, "show":show, "active":active})
|
# return render(request, "backup/local_setup_form.html", {"form": form, "show":show, "active":active})
|
||||||
|
|
||||||
|
|
||||||
# @login_required
|
# @login_required
|
||||||
# @permission_required("backup.change_localbackup")
|
# @permission_required("backup.change_localbackup")
|
||||||
# def local_Backup_stop_or_start(request):
|
# def local_Backup_stop_or_start(request):
|
||||||
@@ -55,7 +56,7 @@ from django.db import connection
|
|||||||
# Parameters:
|
# Parameters:
|
||||||
# request (HttpRequest): The HTTP request object.
|
# request (HttpRequest): The HTTP request object.
|
||||||
|
|
||||||
# Returns:
|
# Returns:
|
||||||
# GET : return local backup setup template
|
# GET : return local backup setup template
|
||||||
# POST : return settings
|
# POST : return settings
|
||||||
# """
|
# """
|
||||||
@@ -98,7 +99,7 @@ from django.db import connection
|
|||||||
@login_required
|
@login_required
|
||||||
@permission_required("backup.add_localbackup")
|
@permission_required("backup.add_localbackup")
|
||||||
def gdrive_setup(request):
|
def gdrive_setup(request):
|
||||||
"""
|
"""
|
||||||
function used to setup gdrive backup.
|
function used to setup gdrive backup.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
@@ -110,7 +111,7 @@ def gdrive_setup(request):
|
|||||||
"""
|
"""
|
||||||
form = GdriveBackupSetupForm()
|
form = GdriveBackupSetupForm()
|
||||||
show = False
|
show = False
|
||||||
active = False
|
active = False
|
||||||
if connection.vendor != "postgresql":
|
if connection.vendor != "postgresql":
|
||||||
return render(request, "backup/404.html")
|
return render(request, "backup/404.html")
|
||||||
if GoogleDriveBackup.objects.exists():
|
if GoogleDriveBackup.objects.exists():
|
||||||
@@ -121,22 +122,29 @@ def gdrive_setup(request):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = GdriveBackupSetupForm(request.POST, request.FILES, instance=instance)
|
form = GdriveBackupSetupForm(request.POST, request.FILES, instance=instance)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
google_drive = form.save()
|
google_drive = form.save()
|
||||||
google_drive.active = False
|
google_drive.active = False
|
||||||
google_drive.save()
|
google_drive.save()
|
||||||
stop_gdrive_backup_job()
|
stop_gdrive_backup_job()
|
||||||
messages.success(request, _("gdrive backup automation setup updated."))
|
messages.success(request, _("gdrive backup automation setup updated."))
|
||||||
return redirect("gdrive")
|
return redirect("gdrive")
|
||||||
return render(request, "backup/gdrive_setup_form.html", {"form": form, "show":show, "active":active})
|
return render(
|
||||||
|
request,
|
||||||
|
"backup/gdrive_setup_form.html",
|
||||||
|
{"form": form, "show": show, "active": active},
|
||||||
|
)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = GdriveBackupSetupForm(request.POST, request.FILES)
|
form = GdriveBackupSetupForm(request.POST, request.FILES)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
form.save()
|
||||||
messages.success(request, _("gdrive backup automation setup Created."))
|
messages.success(request, _("gdrive backup automation setup Created."))
|
||||||
return redirect("gdrive")
|
return redirect("gdrive")
|
||||||
return render(request, "backup/gdrive_setup_form.html", {"form": form, "show":show, "active":active})
|
return render(
|
||||||
|
request,
|
||||||
|
"backup/gdrive_setup_form.html",
|
||||||
|
{"form": form, "show": show, "active": active},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -148,14 +156,14 @@ def gdrive_Backup_stop_or_start(request):
|
|||||||
Parameters:
|
Parameters:
|
||||||
request (HttpRequest): The HTTP request object.
|
request (HttpRequest): The HTTP request object.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
GET : return gdrive backup setup template
|
GET : return gdrive backup setup template
|
||||||
POST : return gdrive backup update template
|
POST : return gdrive backup update template
|
||||||
"""
|
"""
|
||||||
if GoogleDriveBackup.objects.exists():
|
if GoogleDriveBackup.objects.exists():
|
||||||
gdive_backup = GoogleDriveBackup.objects.first()
|
gdive_backup = GoogleDriveBackup.objects.first()
|
||||||
if gdive_backup.active == True:
|
if gdive_backup.active == True:
|
||||||
gdive_backup.active = False
|
gdive_backup.active = False
|
||||||
stop_gdrive_backup_job()
|
stop_gdrive_backup_job()
|
||||||
message = "Gdrive Backup Automation Stopped Successfully."
|
message = "Gdrive Backup Automation Stopped Successfully."
|
||||||
else:
|
else:
|
||||||
@@ -184,4 +192,4 @@ def gdrive_Backup_delete(request):
|
|||||||
gdrive_backup.delete()
|
gdrive_backup.delete()
|
||||||
stop_gdrive_backup_job()
|
stop_gdrive_backup_job()
|
||||||
messages.success(request, _("Gdrive Backup Automation Removed Successfully."))
|
messages.success(request, _("Gdrive Backup Automation Removed Successfully."))
|
||||||
return redirect("gdrive")
|
return redirect("gdrive")
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
def zip_folder(folder_path, output_zip_path):
|
def zip_folder(folder_path, output_zip_path):
|
||||||
with zipfile.ZipFile(output_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
with zipfile.ZipFile(output_zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||||
# Walk the directory
|
# Walk the directory
|
||||||
for root, dirs, files in os.walk(folder_path):
|
for root, dirs, files in os.walk(folder_path):
|
||||||
for file in files:
|
for file in files:
|
||||||
@@ -10,6 +11,3 @@ def zip_folder(folder_path, output_zip_path):
|
|||||||
file_path = os.path.join(root, file)
|
file_path = os.path.join(root, file)
|
||||||
# Add file to zip, preserving the folder structure
|
# Add file to zip, preserving the folder structure
|
||||||
zipf.write(file_path, os.path.relpath(file_path, folder_path))
|
zipf.write(file_path, os.path.relpath(file_path, folder_path))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user