Compare commits
12 Commits
16def1176b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5028a2439e | |||
| 1e78d99624 | |||
| 89d1b15543 | |||
| 82c7457ed1 | |||
| f5adb1000c | |||
| 36abceafb1 | |||
| a9774f41c9 | |||
| 9d88dbc90f | |||
| 89b9c6ea36 | |||
| c380b01e49 | |||
| 2ca60d6feb | |||
| 8a6172f5a7 |
53
.gitea/workflows/deploy.yaml
Normal file
53
.gitea/workflows/deploy.yaml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: Docker Deploy
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build Docker Image
|
||||||
|
run: |
|
||||||
|
docker build -t my-local-app:latest .
|
||||||
|
|
||||||
|
# - name: Remove Old Container
|
||||||
|
# run: |
|
||||||
|
# # Use '|| true' to force a success exit code even if the container is missing
|
||||||
|
# docker stop my-running-app || true
|
||||||
|
# docker rm my-running-app || true
|
||||||
|
- name: Remove Old Container and Free Port
|
||||||
|
run: |
|
||||||
|
# 1. Stop and remove by NAME (what we had before)
|
||||||
|
docker stop my-running-app || true
|
||||||
|
docker rm my-running-app || true
|
||||||
|
|
||||||
|
# 2. EMERGENCY: Stop any container actually using port 9002
|
||||||
|
# This finds the ID of any container bound to 9002 and kills it
|
||||||
|
PORT_OWNER=$(docker ps -q --filter "publish=9002")
|
||||||
|
if [ ! -z "$PORT_OWNER" ]; then
|
||||||
|
docker stop $PORT_OWNER
|
||||||
|
docker rm $PORT_OWNER
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Start New Container
|
||||||
|
run: |
|
||||||
|
docker run -d \
|
||||||
|
--name my-running-app \
|
||||||
|
-p 9002:5005 \
|
||||||
|
--restart unless-stopped \
|
||||||
|
my-local-app:latest
|
||||||
|
|
||||||
|
# - name: Deploy to CasaOS (Docker)
|
||||||
|
# run: |
|
||||||
|
# # Stop and remove the old container if it exists
|
||||||
|
# docker stop my-running-app || true
|
||||||
|
# docker rm my-running-app || true
|
||||||
|
|
||||||
|
# # Run the new container
|
||||||
|
# docker run -d \
|
||||||
|
# --name my-running-app \
|
||||||
|
# -p 9002:5005 \
|
||||||
|
# --restart always \
|
||||||
|
# my-local-app:latest
|
||||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Use a lightweight Python image
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Set the working directory inside the container
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy the requirements file first to leverage Docker cache
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy the rest of your application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose the port Flask runs on (default is 5000)
|
||||||
|
EXPOSE 5005
|
||||||
|
|
||||||
|
# Run the application using Gunicorn for production
|
||||||
|
# Replace 'app:app' with 'your_filename:app_variable_name'
|
||||||
|
# CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
|
||||||
|
CMD ["python", "app.py"]
|
||||||
33
app.py
33
app.py
@@ -26,7 +26,7 @@ def get_file_info(filepath):
|
|||||||
def index():
|
def index():
|
||||||
"""Serves the main UI."""
|
"""Serves the main UI."""
|
||||||
# Default to current working directory
|
# Default to current working directory
|
||||||
start_path = os.getcwd()
|
# start_path = os.getcwd()
|
||||||
return render_template('index.html', start_path='/toor')
|
return render_template('index.html', start_path='/toor')
|
||||||
|
|
||||||
@app.route('/api/scan')
|
@app.route('/api/scan')
|
||||||
@@ -113,6 +113,37 @@ def delete_file():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/delete_multiple', methods=['DELETE'])
|
||||||
|
def delete_multiple_files():
|
||||||
|
"""Deletes multiple files."""
|
||||||
|
data = request.get_json()
|
||||||
|
paths = data.get('paths', [])
|
||||||
|
|
||||||
|
if not paths:
|
||||||
|
return jsonify({"error": "No file paths provided"}), 400
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
success_count = 0
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
if not path or not os.path.exists(path):
|
||||||
|
errors.append({"path": path, "error": "File not found"})
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
if os.path.isdir(path):
|
||||||
|
errors.append({"path": path, "error": "Cannot delete directories"})
|
||||||
|
else:
|
||||||
|
os.remove(path)
|
||||||
|
success_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
errors.append({"path": path, "error": str(e)})
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
return jsonify({"success": True, "message": f"{success_count} files deleted."})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "error": "Some files could not be deleted", "details": errors}), 500
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Run on localhost
|
# Run on localhost
|
||||||
print("Starting File Explorer on http://127.0.0.1:5005")
|
print("Starting File Explorer on http://127.0.0.1:5005")
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
name: Docker Deploy
|
|
||||||
on: [push]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out repository code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Build Docker Image
|
|
||||||
run: |
|
|
||||||
docker build -t my-local-app:latest .
|
|
||||||
|
|
||||||
- name: Deploy to CasaOS (Docker)
|
|
||||||
run: |
|
|
||||||
# Stop and remove the old container if it exists
|
|
||||||
docker stop my-running-app || true
|
|
||||||
docker rm my-running-app || true
|
|
||||||
|
|
||||||
# Run the new container
|
|
||||||
docker run -d \
|
|
||||||
--name my-running-app \
|
|
||||||
-p 9002:5005 \
|
|
||||||
--restart always \
|
|
||||||
my-local-app:latest
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
name: Connectivity Test
|
|
||||||
on: [push]
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo "The runner is working!"
|
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
flask
|
||||||
|
gunicorn
|
||||||
@@ -40,6 +40,9 @@
|
|||||||
<input class="form-check-input" type="checkbox" id="recursiveToggle">
|
<input class="form-check-input" type="checkbox" id="recursiveToggle">
|
||||||
<label class="form-check-label" for="recursiveToggle">Recursive Scan (Subfolders)</label>
|
<label class="form-check-label" for="recursiveToggle">Recursive Scan (Subfolders)</label>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btn-danger ms-auto d-none" id="deleteSelectedBtn" onclick="deleteSelected()">
|
||||||
|
<i class="fa-solid fa-trash-alt me-1"></i>Delete Selected
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,6 +52,7 @@
|
|||||||
<table class="table table-dark table-hover align-middle">
|
<table class="table table-dark table-hover align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th style="width: 30px;"><input type="checkbox" class="form-check-input" id="selectAllCheckbox"></th>
|
||||||
<th class="icon-col"></th>
|
<th class="icon-col"></th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Size</th>
|
<th>Size</th>
|
||||||
@@ -86,8 +90,27 @@
|
|||||||
const API_BASE = '/api';
|
const API_BASE = '/api';
|
||||||
let currentPath = "";
|
let currentPath = "";
|
||||||
|
|
||||||
// Initial Load
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
document.addEventListener('DOMContentLoaded', () => loadFiles());
|
// Initial Load
|
||||||
|
loadFiles();
|
||||||
|
|
||||||
|
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||||
|
const fileTableBody = document.getElementById('fileTableBody');
|
||||||
|
|
||||||
|
selectAllCheckbox.addEventListener('change', (e) => {
|
||||||
|
const checkboxes = fileTableBody.querySelectorAll('.file-checkbox');
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = e.target.checked;
|
||||||
|
});
|
||||||
|
updateDeleteButtonVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
fileTableBody.addEventListener('change', (e) => {
|
||||||
|
if (e.target.classList.contains('file-checkbox')) {
|
||||||
|
updateDeleteButtonVisibility();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
async function loadFiles() {
|
async function loadFiles() {
|
||||||
const path = document.getElementById('pathInput').value;
|
const path = document.getElementById('pathInput').value;
|
||||||
@@ -119,15 +142,36 @@
|
|||||||
|
|
||||||
function renderTable(files) {
|
function renderTable(files) {
|
||||||
const tbody = document.getElementById('fileTableBody');
|
const tbody = document.getElementById('fileTableBody');
|
||||||
|
// tbody is cleared in loadFiles, so we just append.
|
||||||
|
|
||||||
|
// Add "Go Up" link if not at root
|
||||||
|
if (currentPath && currentPath !== '/' && currentPath.length > 1) {
|
||||||
|
let parentPath = currentPath.substring(0, currentPath.lastIndexOf('/'));
|
||||||
|
if (parentPath === '') parentPath = '/';
|
||||||
|
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.className = 'file-row';
|
||||||
|
tr.onclick = () => enterDir(parentPath);
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td></td>
|
||||||
|
<td class="icon-col"><i class="fa-solid fa-arrow-turn-up text-secondary"></i></td>
|
||||||
|
<td>..</td>
|
||||||
|
<td></td>
|
||||||
|
<td class="text-end"></td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="4" class="text-center text-muted">No files found.</td></tr>';
|
// If we haven't added the "up" directory, show no files.
|
||||||
|
if (tbody.childElementCount === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No files found.</td></tr>';
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
files.forEach(file => {
|
files.forEach(file => {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
tr.className = 'file-row';
|
|
||||||
|
|
||||||
// Icon logic
|
// Icon logic
|
||||||
let icon = 'fa-file';
|
let icon = 'fa-file';
|
||||||
@@ -140,15 +184,25 @@
|
|||||||
const size = file.is_dir ? '-' : (file.size / 1024).toFixed(1) + ' KB';
|
const size = file.is_dir ? '-' : (file.size / 1024).toFixed(1) + ' KB';
|
||||||
|
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
|
<td><input type="checkbox" class="form-check-input file-checkbox" data-path="${file.path.replace(/\\/g, '\\\\')}"></td>
|
||||||
<td class="icon-col"><i class="fa-solid ${icon} ${color}"></i></td>
|
<td class="icon-col"><i class="fa-solid ${icon} ${color}"></i></td>
|
||||||
<td onclick="${file.is_dir ? `enterDir('${file.path.replace(/\\/g, '\\\\')}')` : ''}">
|
<td class="file-name-cell">${file.name}</td>
|
||||||
${file.name}
|
|
||||||
</td>
|
|
||||||
<td>${size}</td>
|
<td>${size}</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
${getActions(file)}
|
${getActions(file)}
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Row click should not trigger when clicking on interactive elements
|
||||||
|
tr.addEventListener('click', (e) => {
|
||||||
|
// Ignore clicks on actions, checkboxes, or links
|
||||||
|
if (e.target.closest('button, a, input')) return;
|
||||||
|
|
||||||
|
if (file.is_dir) {
|
||||||
|
enterDir(file.path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -210,7 +264,47 @@
|
|||||||
alert("Network error");
|
alert("Network error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteSelected() {
|
||||||
|
const selectedCheckboxes = document.querySelectorAll('.file-checkbox:checked');
|
||||||
|
if (selectedCheckboxes.length === 0) {
|
||||||
|
alert("Please select files to delete.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm(`Are you sure you want to delete ${selectedCheckboxes.length} selected files? This cannot be undone.`)) return;
|
||||||
|
|
||||||
|
const paths = Array.from(selectedCheckboxes).map(cb => cb.dataset.path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/delete_multiple`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ paths: paths })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
loadFiles(); // Refresh list
|
||||||
|
} else {
|
||||||
|
alert("Error deleting files: " + (data.error || "Unknown error"));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert("Network error during multiple delete.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDeleteButtonVisibility() {
|
||||||
|
const selectedCheckboxes = document.querySelectorAll('.file-checkbox:checked');
|
||||||
|
const deleteBtn = document.getElementById('deleteSelectedBtn');
|
||||||
|
if (selectedCheckboxes.length > 0) {
|
||||||
|
deleteBtn.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
deleteBtn.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user