initial version of subfolder support
This commit is contained in:
parent
e0b2e5f482
commit
e031c7738b
2 changed files with 313 additions and 38 deletions
src/api
128
src/api/app.py
128
src/api/app.py
|
@ -237,40 +237,64 @@ def search():
|
|||
@app.route('/files', methods=['GET'])
|
||||
def list_files():
|
||||
books_dir = "/books"
|
||||
files = []
|
||||
file_tree = []
|
||||
total_files = 0
|
||||
total_size = 0
|
||||
|
||||
try:
|
||||
# Check if indexing is in progress
|
||||
indexing_in_progress = get_progress() is not None
|
||||
|
||||
for filename in os.listdir(books_dir):
|
||||
file_path = os.path.join(books_dir, filename)
|
||||
if os.path.isfile(file_path):
|
||||
file_size = os.path.getsize(file_path)
|
||||
# Extract book title from filename if possible
|
||||
title = filename
|
||||
if ' - ' in filename: # Common pattern in filenames
|
||||
title_parts = filename.split(' - ')
|
||||
if len(title_parts) > 1:
|
||||
title = ' - '.join(title_parts[:-1]) # Take all but last part
|
||||
# Walk through directory structure
|
||||
for root, dirs, files in os.walk(books_dir):
|
||||
# Skip hidden directories
|
||||
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
||||
|
||||
# Get relative path from books_dir
|
||||
rel_path = os.path.relpath(root, books_dir)
|
||||
if rel_path == '.':
|
||||
rel_path = ''
|
||||
|
||||
files.append({
|
||||
'name': filename,
|
||||
'title': title,
|
||||
'path': filename,
|
||||
'size': file_size,
|
||||
'size_mb': round(file_size / (1024 * 1024), 2)
|
||||
# Add directories first
|
||||
for dir_name in sorted(dirs):
|
||||
dir_path = os.path.join(rel_path, dir_name)
|
||||
file_tree.append({
|
||||
'type': 'directory',
|
||||
'name': dir_name,
|
||||
'path': dir_path,
|
||||
'children': []
|
||||
})
|
||||
|
||||
# Add files
|
||||
for file_name in sorted(files):
|
||||
file_path = os.path.join(books_dir, rel_path, file_name)
|
||||
if os.path.isfile(file_path):
|
||||
file_size = os.path.getsize(file_path)
|
||||
# Extract book title from filename if possible
|
||||
title = file_name
|
||||
if ' - ' in file_name: # Common pattern in filenames
|
||||
title_parts = file_name.split(' - ')
|
||||
if len(title_parts) > 1:
|
||||
title = ' - '.join(title_parts[:-1]) # Take all but last part
|
||||
|
||||
full_path = os.path.join(rel_path, file_name)
|
||||
file_tree.append({
|
||||
'type': 'file',
|
||||
'name': file_name,
|
||||
'title': title,
|
||||
'path': full_path,
|
||||
'size': file_size,
|
||||
'size_mb': round(file_size / (1024 * 1024), 2)
|
||||
})
|
||||
total_files += 1
|
||||
total_size += file_size
|
||||
|
||||
# Calculate totals
|
||||
total_files = len(files)
|
||||
total_size = sum(f['size'] for f in files)
|
||||
total_size_mb = round(total_size / (1024 * 1024), 2)
|
||||
|
||||
# If it's an API request, return JSON
|
||||
if request.headers.get('Accept') == 'application/json':
|
||||
return jsonify({
|
||||
'files': files,
|
||||
'file_tree': file_tree,
|
||||
'total_files': total_files,
|
||||
'total_size': total_size,
|
||||
'total_size_mb': total_size_mb,
|
||||
|
@ -279,7 +303,7 @@ def list_files():
|
|||
|
||||
# Otherwise, render the HTML template
|
||||
return render_template('files.html',
|
||||
files=files,
|
||||
file_tree=file_tree,
|
||||
total_files=total_files,
|
||||
total_size=total_size,
|
||||
total_size_mb=total_size_mb,
|
||||
|
@ -607,6 +631,66 @@ def reset_index():
|
|||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/folder_contents/<path:folder_path>', methods=['GET'])
|
||||
def get_folder_contents(folder_path):
|
||||
"""Get contents of a specific folder"""
|
||||
books_dir = "/books"
|
||||
|
||||
# Decode URL-encoded path and normalize
|
||||
decoded_path = unquote(folder_path)
|
||||
# Remove any leading slashes
|
||||
decoded_path = decoded_path.lstrip('/')
|
||||
|
||||
# Join paths safely
|
||||
full_path = os.path.normpath(os.path.join(books_dir, decoded_path))
|
||||
|
||||
# Validate the path is within the books directory
|
||||
if not os.path.abspath(full_path).startswith(os.path.abspath(books_dir)):
|
||||
return jsonify({"error": "Access denied: Folder path outside of books directory"}), 403
|
||||
|
||||
# Check if the path exists and is a directory
|
||||
if not os.path.exists(full_path) or not os.path.isdir(full_path):
|
||||
return jsonify({"error": "Folder not found"}), 404
|
||||
|
||||
try:
|
||||
contents = []
|
||||
|
||||
# List directories first
|
||||
for item in sorted(os.listdir(full_path)):
|
||||
item_path = os.path.join(full_path, item)
|
||||
rel_path = os.path.join(decoded_path, item)
|
||||
|
||||
if os.path.isdir(item_path):
|
||||
contents.append({
|
||||
'type': 'directory',
|
||||
'name': item,
|
||||
'path': rel_path
|
||||
})
|
||||
elif os.path.isfile(item_path):
|
||||
file_size = os.path.getsize(item_path)
|
||||
# Extract book title from filename if possible
|
||||
title = item
|
||||
if ' - ' in item: # Common pattern in filenames
|
||||
title_parts = item.split(' - ')
|
||||
if len(title_parts) > 1:
|
||||
title = ' - '.join(title_parts[:-1]) # Take all but last part
|
||||
|
||||
contents.append({
|
||||
'type': 'file',
|
||||
'name': item,
|
||||
'title': title,
|
||||
'path': rel_path,
|
||||
'size': file_size,
|
||||
'size_mb': round(file_size / (1024 * 1024), 2)
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'folder_path': decoded_path,
|
||||
'contents': contents
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logging.info("Starting the API - inside main block")
|
||||
|
|
|
@ -72,6 +72,43 @@
|
|||
text-decoration: underline;
|
||||
color: #2196f3;
|
||||
}
|
||||
/* Folder styles */
|
||||
.folder-item {
|
||||
cursor: pointer;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.folder-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
color: #2196f3;
|
||||
}
|
||||
.folder-icon {
|
||||
margin-right: 8px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.folder-contents {
|
||||
margin-left: 20px;
|
||||
display: none; /* Hidden by default */
|
||||
}
|
||||
.folder-contents.expanded {
|
||||
display: block;
|
||||
}
|
||||
.breadcrumb {
|
||||
margin-bottom: 15px;
|
||||
padding: 8px 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.breadcrumb a {
|
||||
color: #2196f3;
|
||||
text-decoration: none;
|
||||
}
|
||||
.breadcrumb a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -108,30 +145,184 @@
|
|||
|
||||
<h2>Available Files</h2>
|
||||
|
||||
{% if files %}
|
||||
{% if file_tree %}
|
||||
<ul class="file-list">
|
||||
{% for file in files %}
|
||||
<li class="file-item">
|
||||
<span class="file-name">
|
||||
<a href="/file/{{ file.path }}">
|
||||
{% if file.path.endswith('.epub') %}
|
||||
<br><a href="/file_html/{{ file.path }}" class="plain-view-link">(View as HTML)</a>
|
||||
{% endif %}
|
||||
{% if file.title != file.name %}
|
||||
<span class="book-title">{{ file.title }}</span>
|
||||
<span class="file-name-muted">{{ file.name }}</span>
|
||||
{% for item in file_tree %}
|
||||
{% if item.type == 'directory' %}
|
||||
<li class="folder-item" onclick="toggleFolder(this, event)">
|
||||
<div class="folder-name">
|
||||
<span class="folder-icon">📁</span>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div class="folder-contents" id="folder-{{ item.path|replace('/', '-') }}">
|
||||
<!-- Contents will be populated dynamically -->
|
||||
</div>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="file-item">
|
||||
<span class="file-name">
|
||||
<a href="/file/{{ item.path }}">
|
||||
{% if item.path.endswith('.epub') %}
|
||||
<span class="book-title">{{ item.title }}</span>
|
||||
<span class="file-name-muted">{{ item.name }}</span>
|
||||
<br><a href="/file_html/{{ item.path }}" class="plain-view-link">(View as HTML)</a>
|
||||
{% elif item.title != item.name %}
|
||||
<span class="book-title">{{ item.title }}</span>
|
||||
<span class="file-name-muted">{{ item.name }}</span>
|
||||
{% else %}
|
||||
{{ file.name }}
|
||||
{{ item.name }}
|
||||
{% endif %}
|
||||
</a>
|
||||
</span>
|
||||
<span class="file-size">{{ file.size_mb }} MB</span>
|
||||
</li>
|
||||
</a>
|
||||
</span>
|
||||
<span class="file-size">{{ item.size_mb }} MB</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No files available. Please add files to the books directory.</p>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function toggleFolder(folderElement, event) {
|
||||
// Prevent event from bubbling up to parent folders
|
||||
event.stopPropagation();
|
||||
|
||||
// Toggle the expanded class on the folder contents
|
||||
const contentsElement = folderElement.querySelector('.folder-contents');
|
||||
contentsElement.classList.toggle('expanded');
|
||||
|
||||
// Change folder icon based on state
|
||||
const folderIcon = folderElement.querySelector('.folder-icon');
|
||||
if (contentsElement.classList.contains('expanded')) {
|
||||
folderIcon.textContent = '📂'; // Open folder
|
||||
|
||||
// If this is the first time opening, fetch contents
|
||||
if (contentsElement.getAttribute('data-loaded') !== 'true') {
|
||||
const folderPath = contentsElement.id.replace('folder-', '').replace(/-/g, '/');
|
||||
fetchFolderContents(folderPath, contentsElement);
|
||||
contentsElement.setAttribute('data-loaded', 'true');
|
||||
}
|
||||
} else {
|
||||
folderIcon.textContent = '📁'; // Closed folder
|
||||
}
|
||||
}
|
||||
|
||||
function fetchFolderContents(folderPath, containerElement) {
|
||||
// Show loading indicator
|
||||
containerElement.innerHTML = '<p>Loading folder contents...</p>';
|
||||
|
||||
// Make AJAX request to get folder contents
|
||||
fetch('/folder_contents/' + encodeURIComponent(folderPath))
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load folder contents');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Clear loading message
|
||||
containerElement.innerHTML = '';
|
||||
|
||||
if (data.contents && data.contents.length > 0) {
|
||||
// Create a list for the contents
|
||||
const contentsList = document.createElement('ul');
|
||||
contentsList.className = 'file-list';
|
||||
|
||||
// Add each item to the list
|
||||
data.contents.forEach(item => {
|
||||
const listItem = document.createElement('li');
|
||||
|
||||
if (item.type === 'directory') {
|
||||
// Create folder item
|
||||
listItem.className = 'folder-item';
|
||||
listItem.onclick = function(e) { toggleFolder(this, e); };
|
||||
|
||||
const folderNameDiv = document.createElement('div');
|
||||
folderNameDiv.className = 'folder-name';
|
||||
|
||||
const folderIcon = document.createElement('span');
|
||||
folderIcon.className = 'folder-icon';
|
||||
folderIcon.textContent = '📁';
|
||||
|
||||
folderNameDiv.appendChild(folderIcon);
|
||||
folderNameDiv.appendChild(document.createTextNode(item.name));
|
||||
|
||||
const folderContents = document.createElement('div');
|
||||
folderContents.className = 'folder-contents';
|
||||
folderContents.id = 'folder-' + item.path.replace(/\//g, '-');
|
||||
|
||||
listItem.appendChild(folderNameDiv);
|
||||
listItem.appendChild(folderContents);
|
||||
} else {
|
||||
// Create file item
|
||||
listItem.className = 'file-item';
|
||||
|
||||
const fileNameSpan = document.createElement('span');
|
||||
fileNameSpan.className = 'file-name';
|
||||
|
||||
const fileLink = document.createElement('a');
|
||||
fileLink.href = '/file/' + item.path;
|
||||
|
||||
if (item.path.endsWith('.epub')) {
|
||||
const bookTitle = document.createElement('span');
|
||||
bookTitle.className = 'book-title';
|
||||
bookTitle.textContent = item.title;
|
||||
|
||||
const fileName = document.createElement('span');
|
||||
fileName.className = 'file-name-muted';
|
||||
fileName.textContent = item.name;
|
||||
|
||||
fileLink.appendChild(bookTitle);
|
||||
fileLink.appendChild(fileName);
|
||||
|
||||
const htmlViewLink = document.createElement('a');
|
||||
htmlViewLink.href = '/file_html/' + item.path;
|
||||
htmlViewLink.className = 'plain-view-link';
|
||||
htmlViewLink.textContent = '(View as HTML)';
|
||||
|
||||
fileNameSpan.appendChild(fileLink);
|
||||
fileNameSpan.appendChild(document.createElement('br'));
|
||||
fileNameSpan.appendChild(htmlViewLink);
|
||||
} else if (item.title !== item.name) {
|
||||
const bookTitle = document.createElement('span');
|
||||
bookTitle.className = 'book-title';
|
||||
bookTitle.textContent = item.title;
|
||||
|
||||
const fileName = document.createElement('span');
|
||||
fileName.className = 'file-name-muted';
|
||||
fileName.textContent = item.name;
|
||||
|
||||
fileLink.appendChild(bookTitle);
|
||||
fileLink.appendChild(fileName);
|
||||
fileNameSpan.appendChild(fileLink);
|
||||
} else {
|
||||
fileLink.textContent = item.name;
|
||||
fileNameSpan.appendChild(fileLink);
|
||||
}
|
||||
|
||||
const fileSizeSpan = document.createElement('span');
|
||||
fileSizeSpan.className = 'file-size';
|
||||
fileSizeSpan.textContent = item.size_mb + ' MB';
|
||||
|
||||
listItem.appendChild(fileNameSpan);
|
||||
listItem.appendChild(fileSizeSpan);
|
||||
}
|
||||
|
||||
contentsList.appendChild(listItem);
|
||||
});
|
||||
|
||||
containerElement.appendChild(contentsList);
|
||||
} else {
|
||||
containerElement.innerHTML = '<p>Empty folder</p>';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
containerElement.innerHTML = '<p style="color: red;">Error: ' + error.message + '</p>';
|
||||
console.error('Error fetching folder contents:', error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
|
|
Loading…
Add table
Reference in a new issue