Skip to content

Commit

Permalink
Release AutoArchive 0.0.4
Browse files Browse the repository at this point in the history
- feat: drag to archive/revert (requires PyQt6)
- utility included to patch PyQt6-Qt6 with Universal 2 binary using `lipo`
  • Loading branch information
dingwen07 committed Jun 10, 2024
1 parent 7b5a6f5 commit 6f91d36
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 22 deletions.
131 changes: 112 additions & 19 deletions auto_archive.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,47 @@
import os
import sys
import platform
import shutil
import time
import json
import tempfile
import subprocess

from PyQt6.QtWidgets import QApplication
from PyQt6.QtCore import QEvent, QTimer

ARCHIVE_FOLDER = 'Archive'
ARCHIVE_THRESHOLD = 30 # days
ERROR_LOG_FILE = os.path.join(tempfile.gettempdir(), 'auto_archive.log')
LOG_LEVEL = 'INFO' # INIT, INFO, ERROR
API_LEVEL = 1
API_COMPATIBLE = [None, 1]
EXEC_LOG = []


class HandleOpenDocument(QApplication): # subclass the QApplication class
def __init__(self, argv):
super().__init__(argv)
self.apple_event_open_document = False
self.apple_event_open_document_path = ''

# Setup a timer to wait for file open events
self.timer = QTimer(self)
self.timer.setSingleShot(True)
self.timer.timeout.connect(self.check_file_received)
self.timer.start(1000) # Wait for 1 second before checking

def event(self, event: QEvent): # override the event method
if event.type() == QEvent.Type.FileOpen: # filter the File Open event
file_path = event.file() # get the file name from the event
self.apple_event_open_document = True
self.apple_event_open_document_path = file_path
return True # Mark the event as handled
return super().event(event)

def check_file_received(self):
self.quit() # Quit the application


def get_path():
return os.path.dirname(__file__)
Expand All @@ -31,6 +62,12 @@ def err_log(msg, log_type='ERROR'):
f.write('\n')
else:
f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())} {log_type}: {msg}\n")
EXEC_LOG.append((f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}", log_type, str(msg)))

def exit_log(ret: int):
err_log('Process ended', log_type='INIT')
err_log('', log_type='INIT')
exit(ret)

def revert(run_log, target_dir, archive_folder=ARCHIVE_FOLDER):
err_log(f'Reverting {run_log}', log_type='INIT')
Expand All @@ -42,15 +79,21 @@ def revert(run_log, target_dir, archive_folder=ARCHIVE_FOLDER):
err_log(e)
return

log_api = log.get('api', None)
if log_api not in API_COMPATIBLE:
err_log(f'Incompatible API: {log_api}, STOP', log_type='ERROR')
return

for item in log['moved_files']:
try:
src = os.path.join(target_dir, archive_folder, item['dest'])
dest = os.path.join(target_dir, item['src'])
err_log(f'Reverting {src} to {dest}', log_type='INFO')
dst_t = item['dest'] if 'dest' in item else item['dst'] # backward compatibility
src = os.path.join(target_dir, archive_folder, dst_t)
dst = os.path.join(target_dir, item['src'])
err_log(f'Reverting {src} to {dst}', log_type='INFO')
if os.path.isdir(src):
shutil.copytree(src, dest)
shutil.copytree(src, dst)
else:
shutil.copy2(src, dest)
shutil.copy2(src, dst)
except Exception as e:
err_log(e)
continue
Expand All @@ -59,7 +102,9 @@ def revert(run_log, target_dir, archive_folder=ARCHIVE_FOLDER):


if __name__ == '__main__':
err_log('Process started', log_type='INIT')
err_log(f'Process started with arguments: {sys.argv}', log_type='INIT')
# OS X App Bundle
osx_app_bundle = False
# Mac OS Date Added
osx_date_added = True
by_osx_date_added = True
Expand All @@ -71,6 +116,7 @@ def revert(run_log, target_dir, archive_folder=ARCHIVE_FOLDER):
err_log('Running in OS X App Bundle', log_type='INIT')
self_name = os.path.basename(target_dir)
target_dir = os.path.dirname(target_dir)
osx_app_bundle = True
else:
self_name = os.path.basename(__file__)
else:
Expand All @@ -81,6 +127,35 @@ def revert(run_log, target_dir, archive_folder=ARCHIVE_FOLDER):

err_log(f'Target directory: {target_dir}', log_type='INIT')

# handle Open Document Apple Event
open_document = False
open_document_revert = ''
if osx_app_bundle:
app = HandleOpenDocument(sys.argv)
app.exec()
if app.apple_event_open_document:
err_log(f'Apple Event Open Document Path: {app.apple_event_open_document_path}', log_type='INIT')
open_document = True
# received a file
# if directory, use as target_dir
# if end with .json, use as revert file, the target_dir is .json file/../../..
if os.path.isdir(app.apple_event_open_document_path):
err_log('Archive folder received', log_type='INIT')
target_dir = app.apple_event_open_document_path
elif app.apple_event_open_document_path.endswith('.json'):
err_log('Revert file received', log_type='INIT')
open_document_revert = app.apple_event_open_document_path
target_dir = os.path.dirname(app.apple_event_open_document_path)
target_dir = os.path.dirname(target_dir)
target_dir = os.path.dirname(target_dir)
target_dir = os.path.dirname(target_dir)
else:
err_log('Invalid Apple Event Open Document Path', log_type='ERROR')
err_log(f'Target directory: {target_dir}', log_type='INIT')
else:
err_log('No Apple Event Open Document Path', log_type='INIT')


# get config from config file
config = {
'archive_folder': ARCHIVE_FOLDER,
Expand Down Expand Up @@ -109,14 +184,22 @@ def revert(run_log, target_dir, archive_folder=ARCHIVE_FOLDER):
config = {**config, **load_config}
else:
err_log(f'Config file not found, creating {config_file}', log_type='INIT')
if open_document:
err_log('Open Document received but no config file found, STOP preventing accidental D&D', log_type='ERROR')
exit_log(1)
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=4)
except Exception as e:
err_log(e)
exit()
exit_log(1)

err_log(f'Config: {config}', log_type='INIT')

# set revert file
if open_document_revert != '':
config['revert'] = open_document_revert
err_log(f'Config updated with revert file: {open_document_revert}', log_type='INIT')

archive_folder = config['archive_folder']
archive_threshold = config['archive_threshold']
debug_mode = config['debug']
Expand All @@ -141,13 +224,16 @@ def revert(run_log, target_dir, archive_folder=ARCHIVE_FOLDER):
if 'revert' in config:
if os.path.exists(config['revert']):
err_log('Revert mode', log_type='INIT')
# check if revert file under target_dir/archive_folder
revert_base = os.path.basename(config['revert'])
if os.path.join(target_dir, archive_folder, 'LOG', 'RUN', revert_base) != config['revert']:
err_log(f'Revert file {config["revert"]} not under {target_dir}/{archive_folder}', log_type='ERROR')
exit_log(1)
revert(config['revert'], target_dir, archive_folder)
# move the json to reverted.json
file_name, file_ext = os.path.splitext(config['revert'])
shutil.move(config['revert'], file_name + '_reverted' + file_ext)
err_log('Process ended', log_type='INIT')
err_log('', log_type='INIT')
exit()
shutil.move(config['revert'], f'{file_name}_reverted{file_ext}')
exit_log(0)

# check if archive folder exists
archive_dir = os.path.join(target_dir, archive_folder)
Expand All @@ -156,9 +242,7 @@ def revert(run_log, target_dir, archive_folder=ARCHIVE_FOLDER):
os.mkdir(archive_dir)
except Exception as e:
err_log(e)
err_log('Process ended', log_type='INIT')
err_log('', log_type='INIT')
exit()
exit_log(1)


# check all files under target directory
Expand Down Expand Up @@ -223,21 +307,29 @@ def revert(run_log, target_dir, archive_folder=ARCHIVE_FOLDER):
file_archive_path = os.path.join(file_archive_dir, file)
err_log(f'Init file archive path: {file_archive_path}', log_type='INFO')
file_relative = file
file_name, file_ext = os.path.splitext(file)
i = 1
while os.path.exists(file_archive_path):
file_relative = f'{file_name} ({i}){file_ext}'
file_archive_path = os.path.join(file_archive_dir, file_relative)
i += 1
'''
if os.path.exists(file_archive_path):
file_name, file_ext = os.path.splitext(file)
i = 1
while True:
file_relative = file_name + ' (' + str(i) + ')' + file_ext
file_relative = f'{file_name} ({i}){file_ext}'
file_archive_path = os.path.join(file_archive_dir, file_relative)
if not os.path.exists(file_archive_path):
break
i += 1
'''
err_log(f'Final file archive path: {file_archive_path}', log_type='INFO')
shutil.move(file_path, file_archive_path)
# success
moved_files.append({
'src': file,
'dest': os.path.join(file_archive_dir_relative, file_relative)
'dst': os.path.join(file_archive_dir_relative, file_relative)
})
except Exception as e:
# save error to debug log
Expand Down Expand Up @@ -275,12 +367,13 @@ def revert(run_log, target_dir, archive_folder=ARCHIVE_FOLDER):
try:
with open(run_log_file, 'w') as f:
json.dump({
'api': API_LEVEL,
'start_time': start_time,
'end_time': end_time,
'moved_files': moved_files
'moved_files': moved_files,
'exec_log': EXEC_LOG
}, f, indent=4)
except Exception as e:
err_log(e)

err_log('Process ended', log_type='INIT')
err_log('', log_type='INIT')
exit_log(0)
7 changes: 5 additions & 2 deletions build-universal.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ PYTHON=/usr/bin/python3
export PATH=/usr/bin:$PATH

# Hello
echo "AutoArchive Universal Build Utility"
echo "AutoArchive Universal 2 Binary Build Utility"

if [ "$(arch)" == "arm64" ]; then
echo "Running on arm64, switching to x86_64"
arch -x86_64 $0
exit
fi

echo "Running on $(arch)"
echo "Build Directory: $BUILD_DIR"

Expand Down Expand Up @@ -53,9 +54,11 @@ echo "Installing dependencies..."
source ./venv/bin/activate
pip install -r requirements.txt
deactivate
echo "Patching PyQt6..."
./pyqt6-universal-patch.sh

echo "Running Build Utility..."
./build.sh

echo "Build Utility complete."
echo "Universal 2 Binary Build Utility complete."
echo "Run 'cd ./build/universal' for next steps."
6 changes: 6 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,10 @@ fi
source ./venv/bin/activate

python3 setup.py py2app

deactivate

echo "Copying artifact to ./test"
cp -R ./dist/AutoArchive.app ./test/AutoArchive.app

echo "Build Utility complete."
37 changes: 37 additions & 0 deletions pyqt6-universal-patch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/bash

# functions
trash() { osascript -e "tell application \"Finder\" to delete POSIX file \"$(realpath "$1")\"" > /dev/null; }

echo "PyQt Universal 2 Binary Patch Utility"

if [[ "$(sysctl -n machdep.cpu.brand_string)" != *"Apple"* ]]; then
echo "This script must be run on an Apple Silicon Mac. Nothing to do."
echo "PyQt Universal 2 Binary Patch Utility complete."
exit
fi

echo "Current Directory: $(pwd)"

echo "Activating virtual environment..."
source ./venv/bin/activate

SITE_PACKAGE=$(python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")
echo "Site Package: $SITE_PACKAGE"

echo "Patching PyQt6..."
# make a backup, preserve permissions
cp -R $SITE_PACKAGE/PyQt6 $SITE_PACKAGE/PyQt6.bak

# reinstall PyQt6 as arm64
arch -arm64 pip install PyQt6 --force-reinstall

# Patch
cd $SITE_PACKAGE
echo "Converting to Universal Binary..."
find ./PyQt6/Qt6 -type f -perm +111 -exec sh -c 'xcrun lipo -create -output "{}" "{}" "$(echo "{}" | sed "s|^./PyQt6/|./PyQt6.bak/|")"' \;
echo "Patching complete."

deactivate

echo "PyQt Universal 2 Binary Patch Utility complete."
6 changes: 6 additions & 0 deletions release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ read -p "Press Enter to continue to codesign"

# codesign
CODESIGN_VARS="--deep --force --verify --verbose --timestamp --options runtime"
echo Codesigning libraries...
find $source/ -name "*.so" -exec codesign $CODESIGN_VARS -s "$TEAM_ID" {} \;
find $source/ -name "*.dylib" -exec codesign $CODESIGN_VARS -s "$TEAM_ID" {} \;
echo Codesigning executables...
find $source/ -type f -perm +111 -exec codesign $CODESIGN_VARS -s "$TEAM_ID" {} \;
codesign $CODESIGN_VARS -s "$TEAM_ID" $source

echo .
Expand All @@ -50,3 +54,5 @@ spctl -vvv --assess --type exec $source

trash $source_parent/AutoArchive.zip
/usr/bin/ditto -c -k --keepParent $source $source_parent/AutoArchive.zip
mkdir -p $source_parent/AutoArchive
cp -R $source $source_parent/AutoArchive/AutoArchive.app
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
py2app~=0.28.7
setuptools<70.0.0
wheel
pyqt6
19 changes: 18 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,34 @@

APP = ['auto_archive.py']
DATA_FILES = []
VERSION = '0.0.3'
VERSION = '0.0.4'

PLIST = {
'CFBundleShortVersionString': VERSION,
'CFBundleGetInfoString': f'AutoArchive {VERSION}',
'CFBundleIdentifier': 'net.extrawdw.AutoArchive',
'NSHumanReadableCopyright': f'Copyright © {datetime.now().year} Dingwen Wang. All rights reserved.',
# support JSON file as input
'CFBundleDocumentTypes': [
{
'CFBundleTypeExtensions': ['json'],
'CFBundleTypeName': 'Run Log JSON',
'CFBundleTypeRole': 'Editor',
'LSItemContentTypes': ['public.json'],
},
{
'CFBundleTypeExtensions': [],
'CFBundleTypeName': 'Folder',
'CFBundleTypeRole': 'Editor',
'LSItemContentTypes': ['public.folder'],
}
],
}

OPTIONS = {
'iconfile': '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/AllMyFiles.icns',
'plist': PLIST,
'arch': 'universal2',
}

setup(
Expand Down

0 comments on commit 6f91d36

Please sign in to comment.