mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 09:49:46 +00:00
Yattee v2 rewrite
This commit is contained in:
504
Yattee/Services/MediaSources/SMBBridge/SMBBridge.c
Normal file
504
Yattee/Services/MediaSources/SMBBridge/SMBBridge.c
Normal file
@@ -0,0 +1,504 @@
|
||||
//
|
||||
// SMBBridge.c
|
||||
// Yattee
|
||||
//
|
||||
// C implementation of libsmbclient bridge for directory browsing using context-specific
|
||||
// function pointers. This provides complete isolation from other libsmbclient users
|
||||
// (e.g., FFmpeg in MPV) by avoiding smbc_set_context() which modifies global state.
|
||||
//
|
||||
|
||||
#include "SMBBridge.h"
|
||||
#include "libsmbclient_minimal.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <pthread.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// Context wrapper that bundles SMBCCTX with auth data
|
||||
struct SMBContextWrapper {
|
||||
SMBCCTX *ctx; // Isolated libsmbclient context
|
||||
char workgroup[128];
|
||||
char username[128];
|
||||
char password[128];
|
||||
SMBProtocolVersion version;
|
||||
pthread_mutex_t mutex; // Per-context mutex for thread safety
|
||||
};
|
||||
|
||||
// Authentication callback for libsmbclient context (called during SMB operations)
|
||||
static void auth_fn_with_context(
|
||||
SMBCCTX *ctx,
|
||||
const char *server, const char *share,
|
||||
char *workgroup, int wgmaxlen,
|
||||
char *username, int unmaxlen,
|
||||
char *password, int pwmaxlen
|
||||
) {
|
||||
fprintf(stderr, "[SMBBridge] Auth callback invoked for server: %s, share: %s\n",
|
||||
server ? server : "(null)", share ? share : "(null)");
|
||||
|
||||
// Get auth data from context's user data
|
||||
struct SMBContextWrapper *wrapper = (struct SMBContextWrapper *)smbc_getOptionUserData(ctx);
|
||||
if (wrapper) {
|
||||
fprintf(stderr, "[SMBBridge] Auth: using workgroup=%s, username=%s, has_password=%s\n",
|
||||
wrapper->workgroup,
|
||||
wrapper->username[0] ? wrapper->username : "(empty)",
|
||||
wrapper->password[0] ? "yes" : "no");
|
||||
|
||||
if (wrapper->workgroup[0]) {
|
||||
strncpy(workgroup, wrapper->workgroup, wgmaxlen - 1);
|
||||
workgroup[wgmaxlen - 1] = '\0';
|
||||
}
|
||||
if (wrapper->username[0]) {
|
||||
strncpy(username, wrapper->username, unmaxlen - 1);
|
||||
username[unmaxlen - 1] = '\0';
|
||||
}
|
||||
if (wrapper->password[0]) {
|
||||
strncpy(password, wrapper->password, pwmaxlen - 1);
|
||||
password[pwmaxlen - 1] = '\0';
|
||||
}
|
||||
} else {
|
||||
fprintf(stderr, "[SMBBridge] Auth: WARNING - no wrapper found!\n");
|
||||
}
|
||||
}
|
||||
|
||||
void* smb_init_context(const char *workgroup,
|
||||
const char *username,
|
||||
const char *password,
|
||||
SMBProtocolVersion version) {
|
||||
fprintf(stderr, "[SMBBridge] Creating new isolated SMB context\n");
|
||||
|
||||
// Allocate context wrapper
|
||||
struct SMBContextWrapper *wrapper = (struct SMBContextWrapper *)calloc(1, sizeof(struct SMBContextWrapper));
|
||||
if (!wrapper) {
|
||||
fprintf(stderr, "[SMBBridge] Failed to allocate context wrapper\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Initialize mutex for this context
|
||||
pthread_mutexattr_t attr;
|
||||
pthread_mutexattr_init(&attr);
|
||||
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
|
||||
pthread_mutex_init(&wrapper->mutex, &attr);
|
||||
pthread_mutexattr_destroy(&attr);
|
||||
|
||||
// Copy credentials
|
||||
strncpy(wrapper->workgroup, workgroup ? workgroup : "WORKGROUP", sizeof(wrapper->workgroup) - 1);
|
||||
strncpy(wrapper->username, username ? username : "", sizeof(wrapper->username) - 1);
|
||||
strncpy(wrapper->password, password ? password : "", sizeof(wrapper->password) - 1);
|
||||
wrapper->version = version;
|
||||
|
||||
// Create NEW isolated libsmbclient context
|
||||
wrapper->ctx = smbc_new_context();
|
||||
if (!wrapper->ctx) {
|
||||
fprintf(stderr, "[SMBBridge] Failed to create libsmbclient context\n");
|
||||
pthread_mutex_destroy(&wrapper->mutex);
|
||||
free(wrapper);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Configure THIS context only (does not affect global state or other contexts)
|
||||
smbc_setFunctionAuthDataWithContext(wrapper->ctx, auth_fn_with_context);
|
||||
smbc_setOptionUserData(wrapper->ctx, wrapper);
|
||||
smbc_setTimeout(wrapper->ctx, 10000); // 10 second timeout
|
||||
smbc_setWorkgroup(wrapper->ctx, wrapper->workgroup);
|
||||
if (wrapper->username[0]) {
|
||||
smbc_setUser(wrapper->ctx, wrapper->username);
|
||||
}
|
||||
|
||||
// Set protocol version BEFORE initialization (must be set before smbc_init_context)
|
||||
// Note: We set min_proto to allow negotiation, and max_proto to limit the highest version
|
||||
if (wrapper->version != SMB_PROTOCOL_AUTO) {
|
||||
const char *min_proto = "NT1"; // Allow negotiation from SMB1
|
||||
const char *max_proto = NULL;
|
||||
switch (wrapper->version) {
|
||||
case SMB_PROTOCOL_SMB1:
|
||||
max_proto = "NT1";
|
||||
min_proto = "NT1"; // Force SMB1 only
|
||||
break;
|
||||
case SMB_PROTOCOL_SMB2:
|
||||
max_proto = "SMB2";
|
||||
break;
|
||||
case SMB_PROTOCOL_SMB3:
|
||||
max_proto = "SMB3";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (max_proto) {
|
||||
fprintf(stderr, "[SMBBridge] Setting protocol range: %s to %s\n", min_proto, max_proto);
|
||||
smbc_bool result = smbc_setOptionProtocols(wrapper->ctx, min_proto, max_proto);
|
||||
fprintf(stderr, "[SMBBridge] smbc_setOptionProtocols returned: %d\n", result);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize context AFTER all options are set
|
||||
if (smbc_init_context(wrapper->ctx) == NULL) {
|
||||
fprintf(stderr, "[SMBBridge] Failed to initialize libsmbclient context\n");
|
||||
smbc_free_context(wrapper->ctx, 0);
|
||||
pthread_mutex_destroy(&wrapper->mutex);
|
||||
free(wrapper);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[SMBBridge] Successfully created isolated SMB context (workgroup: %s, user: %s)\n",
|
||||
wrapper->workgroup, wrapper->username[0] ? wrapper->username : "(guest)");
|
||||
|
||||
return (void *)wrapper;
|
||||
}
|
||||
|
||||
void smb_free_context(void *ctx_ptr) {
|
||||
if (!ctx_ptr) return;
|
||||
|
||||
struct SMBContextWrapper *wrapper = (struct SMBContextWrapper *)ctx_ptr;
|
||||
|
||||
fprintf(stderr, "[SMBBridge] Freeing SMB context\n");
|
||||
|
||||
// Lock before cleanup
|
||||
pthread_mutex_lock(&wrapper->mutex);
|
||||
|
||||
// Free libsmbclient context
|
||||
if (wrapper->ctx) {
|
||||
smbc_free_context(wrapper->ctx, 1); // shutdown_ctx = 1
|
||||
wrapper->ctx = NULL;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&wrapper->mutex);
|
||||
pthread_mutex_destroy(&wrapper->mutex);
|
||||
|
||||
// Clear sensitive data
|
||||
memset(wrapper->password, 0, sizeof(wrapper->password));
|
||||
memset(wrapper->username, 0, sizeof(wrapper->username));
|
||||
|
||||
free(wrapper);
|
||||
|
||||
fprintf(stderr, "[SMBBridge] SMB context freed\n");
|
||||
}
|
||||
|
||||
SMBFileInfo* smb_list_directory(void *ctx_ptr,
|
||||
const char *url,
|
||||
int *count,
|
||||
char **error) {
|
||||
*count = 0;
|
||||
*error = NULL;
|
||||
|
||||
if (!ctx_ptr || !url) {
|
||||
if (error) {
|
||||
*error = strdup("Invalid parameters");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct SMBContextWrapper *wrapper = (struct SMBContextWrapper *)ctx_ptr;
|
||||
|
||||
// Lock THIS context's mutex for thread safety
|
||||
pthread_mutex_lock(&wrapper->mutex);
|
||||
|
||||
fprintf(stderr, "[SMBBridge] Listing directory: %s\n", url);
|
||||
|
||||
// Get context-specific function pointers (avoids smbc_set_context which conflicts with MPV/FFmpeg)
|
||||
smbc_opendir_fn opendir_fn = smbc_getFunctionOpendir(wrapper->ctx);
|
||||
smbc_readdir_fn readdir_fn = smbc_getFunctionReaddir(wrapper->ctx);
|
||||
smbc_closedir_fn closedir_fn = smbc_getFunctionClosedir(wrapper->ctx);
|
||||
smbc_lseekdir_fn lseekdir_fn = smbc_getFunctionLseekdir(wrapper->ctx);
|
||||
smbc_stat_fn stat_fn = smbc_getFunctionStat(wrapper->ctx);
|
||||
|
||||
// Detect if we're listing shares at the server root
|
||||
// URL format: "smb://server/" - no path after server
|
||||
int is_listing_shares = 0;
|
||||
{
|
||||
const char *server_start = strstr(url, "://");
|
||||
if (server_start) {
|
||||
server_start += 3; // Skip "://"
|
||||
const char *first_slash = strchr(server_start, '/');
|
||||
if (first_slash) {
|
||||
const char *path_start = first_slash + 1;
|
||||
if (*path_start == '\0' || (*path_start == '/' && *(path_start + 1) == '\0')) {
|
||||
is_listing_shares = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "[SMBBridge] Listing mode: %s\n", is_listing_shares ? "shares" : "files/dirs");
|
||||
|
||||
// Open directory using context-specific function
|
||||
errno = 0;
|
||||
SMBCFILE *dir = opendir_fn(wrapper->ctx, url);
|
||||
int saved_errno = errno;
|
||||
|
||||
if (!dir) {
|
||||
fprintf(stderr, "[SMBBridge] Failed to open directory: %s (errno: %d)\n",
|
||||
strerror(saved_errno), saved_errno);
|
||||
pthread_mutex_unlock(&wrapper->mutex);
|
||||
if (error) {
|
||||
char err_buf[256];
|
||||
snprintf(err_buf, sizeof(err_buf), "Failed to open directory: %s (errno: %d)",
|
||||
strerror(saved_errno), saved_errno);
|
||||
*error = strdup(err_buf);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// First pass: count valid entries
|
||||
struct smbc_dirent *dirent;
|
||||
int entry_count = 0;
|
||||
while ((dirent = readdir_fn(wrapper->ctx, dir)) != NULL) {
|
||||
// Skip . and ..
|
||||
if (strcmp(dirent->name, ".") == 0 || strcmp(dirent->name, "..") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_listing_shares) {
|
||||
// When listing shares, only count SMBC_FILE_SHARE (type 3)
|
||||
if (dirent->smbc_type == SMBC_FILE_SHARE) {
|
||||
entry_count++;
|
||||
}
|
||||
} else {
|
||||
// When listing directory contents, count files and directories
|
||||
if (dirent->smbc_type == SMBC_DIR || dirent->smbc_type == SMBC_FILE) {
|
||||
entry_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty directory is valid (not an error)
|
||||
if (entry_count == 0) {
|
||||
fprintf(stderr, "[SMBBridge] Empty directory\n");
|
||||
closedir_fn(wrapper->ctx, dir);
|
||||
pthread_mutex_unlock(&wrapper->mutex);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[SMBBridge] Found %d entries\n", entry_count);
|
||||
|
||||
// Allocate result array
|
||||
SMBFileInfo *files = (SMBFileInfo *)calloc(entry_count, sizeof(SMBFileInfo));
|
||||
if (!files) {
|
||||
fprintf(stderr, "[SMBBridge] Out of memory\n");
|
||||
if (error) {
|
||||
*error = strdup("Out of memory");
|
||||
}
|
||||
closedir_fn(wrapper->ctx, dir);
|
||||
pthread_mutex_unlock(&wrapper->mutex);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Second pass: populate array (seek back to start)
|
||||
lseekdir_fn(wrapper->ctx, dir, 0);
|
||||
|
||||
int i = 0;
|
||||
while ((dirent = readdir_fn(wrapper->ctx, dir)) != NULL && i < entry_count) {
|
||||
// Skip . and ..
|
||||
if (strcmp(dirent->name, ".") == 0 || strcmp(dirent->name, "..") == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter based on listing mode
|
||||
int should_include = 0;
|
||||
if (is_listing_shares) {
|
||||
should_include = (dirent->smbc_type == SMBC_FILE_SHARE);
|
||||
} else {
|
||||
should_include = (dirent->smbc_type == SMBC_DIR || dirent->smbc_type == SMBC_FILE);
|
||||
}
|
||||
|
||||
if (!should_include) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Copy name
|
||||
files[i].name = strdup(dirent->name);
|
||||
files[i].type = dirent->smbc_type;
|
||||
|
||||
// Build full path for stat
|
||||
size_t url_len = strlen(url);
|
||||
size_t name_len = strlen(dirent->name);
|
||||
char *full_path = (char *)malloc(url_len + name_len + 2);
|
||||
if (full_path) {
|
||||
strcpy(full_path, url);
|
||||
if (url[url_len - 1] != '/') {
|
||||
strcat(full_path, "/");
|
||||
}
|
||||
strcat(full_path, dirent->name);
|
||||
|
||||
// Get detailed file info using context-specific function
|
||||
struct stat st;
|
||||
if (stat_fn(wrapper->ctx, full_path, &st) == 0) {
|
||||
files[i].size = st.st_size;
|
||||
files[i].mtime = st.st_mtime;
|
||||
files[i].ctime = st.st_ctime;
|
||||
} else {
|
||||
// If stat fails, use defaults
|
||||
files[i].size = 0;
|
||||
files[i].mtime = 0;
|
||||
files[i].ctime = 0;
|
||||
}
|
||||
|
||||
free(full_path);
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
closedir_fn(wrapper->ctx, dir);
|
||||
|
||||
pthread_mutex_unlock(&wrapper->mutex);
|
||||
|
||||
fprintf(stderr, "[SMBBridge] Directory listing complete. Count: %d\n", i);
|
||||
|
||||
*count = i;
|
||||
return files;
|
||||
}
|
||||
|
||||
void smb_free_file_list(SMBFileInfo *files, int count) {
|
||||
if (!files) return;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (files[i].name) {
|
||||
free(files[i].name);
|
||||
}
|
||||
}
|
||||
free(files);
|
||||
}
|
||||
|
||||
int smb_test_connection(void *ctx_ptr, const char *url) {
|
||||
if (!ctx_ptr || !url) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
struct SMBContextWrapper *wrapper = (struct SMBContextWrapper *)ctx_ptr;
|
||||
|
||||
// Lock context mutex
|
||||
pthread_mutex_lock(&wrapper->mutex);
|
||||
|
||||
fprintf(stderr, "[SMBBridge] Testing connection to: %s\n", url);
|
||||
|
||||
// Get context-specific function pointers (avoids smbc_set_context which conflicts with MPV/FFmpeg)
|
||||
smbc_opendir_fn opendir_fn = smbc_getFunctionOpendir(wrapper->ctx);
|
||||
smbc_closedir_fn closedir_fn = smbc_getFunctionClosedir(wrapper->ctx);
|
||||
|
||||
// Try to open directory using context-specific function
|
||||
errno = 0;
|
||||
SMBCFILE *dir = opendir_fn(wrapper->ctx, url);
|
||||
int saved_errno = errno;
|
||||
|
||||
if (!dir) {
|
||||
fprintf(stderr, "[SMBBridge] Connection test failed: %s (errno: %d)\n",
|
||||
strerror(saved_errno), saved_errno);
|
||||
pthread_mutex_unlock(&wrapper->mutex);
|
||||
return -saved_errno;
|
||||
}
|
||||
|
||||
closedir_fn(wrapper->ctx, dir);
|
||||
|
||||
pthread_mutex_unlock(&wrapper->mutex);
|
||||
|
||||
fprintf(stderr, "[SMBBridge] Connection test succeeded\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int smb_download_file(void *ctx_ptr, const char *url, const char *local_path, char **error) {
|
||||
*error = NULL;
|
||||
|
||||
if (!ctx_ptr || !url || !local_path) {
|
||||
if (error) {
|
||||
*error = strdup("Invalid parameters");
|
||||
}
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
struct SMBContextWrapper *wrapper = (struct SMBContextWrapper *)ctx_ptr;
|
||||
|
||||
// Lock context mutex
|
||||
pthread_mutex_lock(&wrapper->mutex);
|
||||
|
||||
fprintf(stderr, "[SMBBridge] Downloading file: %s -> %s\n", url, local_path);
|
||||
|
||||
// Get context-specific function pointers (avoids smbc_set_context which conflicts with MPV/FFmpeg)
|
||||
smbc_open_fn open_fn = smbc_getFunctionOpen(wrapper->ctx);
|
||||
smbc_read_fn read_fn = smbc_getFunctionRead(wrapper->ctx);
|
||||
smbc_close_fn close_fn = smbc_getFunctionClose(wrapper->ctx);
|
||||
|
||||
// Open remote file for reading using context-specific function
|
||||
errno = 0;
|
||||
SMBCFILE *file = open_fn(wrapper->ctx, url, O_RDONLY, 0);
|
||||
int saved_errno = errno;
|
||||
|
||||
if (!file) {
|
||||
fprintf(stderr, "[SMBBridge] Failed to open SMB file: %s (errno: %d)\n",
|
||||
strerror(saved_errno), saved_errno);
|
||||
pthread_mutex_unlock(&wrapper->mutex);
|
||||
if (error) {
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf), "Failed to open SMB file: %s (errno: %d)",
|
||||
strerror(saved_errno), saved_errno);
|
||||
*error = strdup(buf);
|
||||
}
|
||||
return -saved_errno;
|
||||
}
|
||||
|
||||
// Open local file for writing
|
||||
FILE *local_file = fopen(local_path, "wb");
|
||||
if (!local_file) {
|
||||
int local_errno = errno;
|
||||
fprintf(stderr, "[SMBBridge] Failed to create local file: %s (errno: %d)\n",
|
||||
strerror(local_errno), local_errno);
|
||||
close_fn(wrapper->ctx, file);
|
||||
pthread_mutex_unlock(&wrapper->mutex);
|
||||
if (error) {
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf), "Failed to create local file: %s (errno: %d)",
|
||||
strerror(local_errno), local_errno);
|
||||
*error = strdup(buf);
|
||||
}
|
||||
return -local_errno;
|
||||
}
|
||||
|
||||
// Read from SMB and write to local file
|
||||
char buffer[8192];
|
||||
ssize_t bytes_read;
|
||||
size_t total_bytes = 0;
|
||||
|
||||
while ((bytes_read = read_fn(wrapper->ctx, file, buffer, sizeof(buffer))) > 0) {
|
||||
size_t bytes_written = fwrite(buffer, 1, bytes_read, local_file);
|
||||
if (bytes_written != (size_t)bytes_read) {
|
||||
// Write error
|
||||
int local_errno = errno;
|
||||
fprintf(stderr, "[SMBBridge] Failed to write to local file (errno: %d)\n", local_errno);
|
||||
fclose(local_file);
|
||||
close_fn(wrapper->ctx, file);
|
||||
pthread_mutex_unlock(&wrapper->mutex);
|
||||
unlink(local_path); // Clean up partial file
|
||||
if (error) {
|
||||
*error = strdup("Failed to write to local file");
|
||||
}
|
||||
return -local_errno;
|
||||
}
|
||||
total_bytes += bytes_written;
|
||||
}
|
||||
|
||||
// Check for read errors
|
||||
if (bytes_read < 0) {
|
||||
int read_errno = errno;
|
||||
fprintf(stderr, "[SMBBridge] Failed to read from SMB: %s\n", strerror(read_errno));
|
||||
fclose(local_file);
|
||||
close_fn(wrapper->ctx, file);
|
||||
pthread_mutex_unlock(&wrapper->mutex);
|
||||
unlink(local_path); // Clean up partial file
|
||||
if (error) {
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf), "Failed to read from SMB: %s", strerror(read_errno));
|
||||
*error = strdup(buf);
|
||||
}
|
||||
return -read_errno;
|
||||
}
|
||||
|
||||
// Clean up
|
||||
fclose(local_file);
|
||||
close_fn(wrapper->ctx, file);
|
||||
pthread_mutex_unlock(&wrapper->mutex);
|
||||
|
||||
fprintf(stderr, "[SMBBridge] Download complete: %zu bytes\n", total_bytes);
|
||||
return 0;
|
||||
}
|
||||
74
Yattee/Services/MediaSources/SMBBridge/SMBBridge.h
Normal file
74
Yattee/Services/MediaSources/SMBBridge/SMBBridge.h
Normal file
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// SMBBridge.h
|
||||
// Yattee
|
||||
//
|
||||
// C bridge to libsmbclient for SMB directory browsing.
|
||||
//
|
||||
|
||||
#ifndef SMBBridge_h
|
||||
#define SMBBridge_h
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <time.h>
|
||||
|
||||
// SMB protocol version options
|
||||
typedef enum {
|
||||
SMB_PROTOCOL_AUTO = 0,
|
||||
SMB_PROTOCOL_SMB1 = 1,
|
||||
SMB_PROTOCOL_SMB2 = 2,
|
||||
SMB_PROTOCOL_SMB3 = 3
|
||||
} SMBProtocolVersion;
|
||||
|
||||
// File information structure for Swift interop
|
||||
typedef struct {
|
||||
char *name; // File/directory name (caller must free)
|
||||
unsigned int type; // SMBC_DIR=7, SMBC_FILE=8
|
||||
off_t size; // File size in bytes
|
||||
time_t mtime; // Modification time
|
||||
time_t ctime; // Creation/change time
|
||||
} SMBFileInfo;
|
||||
|
||||
// Initialize SMB context with authentication and protocol preferences
|
||||
// Returns NULL on failure
|
||||
// Parameters:
|
||||
// workgroup: Workgroup/domain name (e.g., "WORKGROUP")
|
||||
// username: Username for authentication (NULL for guest access)
|
||||
// password: Password for authentication (NULL for guest access)
|
||||
// version: SMB protocol version preference
|
||||
void* smb_init_context(const char *workgroup,
|
||||
const char *username,
|
||||
const char *password,
|
||||
SMBProtocolVersion version);
|
||||
|
||||
// Clean up SMB context and free resources
|
||||
void smb_free_context(void *ctx);
|
||||
|
||||
// List directory contents at given SMB URL
|
||||
// Returns array of SMBFileInfo (caller must free with smb_free_file_list)
|
||||
// Parameters:
|
||||
// ctx: Context from smb_init_context
|
||||
// url: Full SMB URL (e.g., "smb://server/share/path")
|
||||
// count: Output parameter - number of items returned
|
||||
// error: Output parameter - error message if failed (caller must free)
|
||||
SMBFileInfo* smb_list_directory(void *ctx,
|
||||
const char *url,
|
||||
int *count,
|
||||
char **error);
|
||||
|
||||
// Free directory listing returned by smb_list_directory
|
||||
void smb_free_file_list(SMBFileInfo *files, int count);
|
||||
|
||||
// Test connection to SMB URL
|
||||
// Returns 0 on success, negative error code on failure
|
||||
int smb_test_connection(void *ctx, const char *url);
|
||||
|
||||
// Download file from SMB to local path
|
||||
// Returns 0 on success, negative error code on failure
|
||||
// Parameters:
|
||||
// ctx: Context from smb_init_context
|
||||
// url: Full SMB URL (e.g., "smb://server/share/path/file.srt")
|
||||
// local_path: Local filesystem path to write to
|
||||
// error: Output parameter - error message if failed (caller must free)
|
||||
int smb_download_file(void *ctx, const char *url, const char *local_path, char **error);
|
||||
|
||||
#endif /* SMBBridge_h */
|
||||
233
Yattee/Services/MediaSources/SMBBridge/SMBBridgeWrapper.swift
Normal file
233
Yattee/Services/MediaSources/SMBBridge/SMBBridgeWrapper.swift
Normal file
@@ -0,0 +1,233 @@
|
||||
//
|
||||
// SMBBridgeWrapper.swift
|
||||
// Yattee
|
||||
//
|
||||
// Swift wrapper around libsmbclient C bridge for directory browsing.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Swift wrapper for SMB file information from C bridge.
|
||||
struct SMBFileEntry: Sendable {
|
||||
let name: String
|
||||
let isDirectory: Bool
|
||||
let isShare: Bool // True if this is an SMB file share (SMBC_FILE_SHARE)
|
||||
let size: Int64
|
||||
let modifiedDate: Date?
|
||||
let createdDate: Date?
|
||||
}
|
||||
|
||||
/// Error types for SMB bridge operations.
|
||||
enum SMBBridgeError: Error, LocalizedError, Sendable {
|
||||
case contextInitFailed
|
||||
case connectionFailed(String)
|
||||
case listingFailed(String)
|
||||
case invalidURL
|
||||
case invalidParameters
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .contextInitFailed:
|
||||
return "Failed to initialize SMB context"
|
||||
case .connectionFailed(let msg):
|
||||
return "SMB connection failed: \(msg)"
|
||||
case .listingFailed(let msg):
|
||||
return "Failed to list directory: \(msg)"
|
||||
case .invalidURL:
|
||||
return "Invalid SMB URL"
|
||||
case .invalidParameters:
|
||||
return "Invalid parameters provided"
|
||||
}
|
||||
}
|
||||
|
||||
/// User-friendly error messages based on common SMB errors
|
||||
var userFriendlyMessage: String {
|
||||
switch self {
|
||||
case .connectionFailed(let msg) where msg.contains("errno: 13"):
|
||||
return "Permission denied. Check username and password."
|
||||
case .connectionFailed(let msg) where msg.contains("errno: 2"):
|
||||
return "Share or path not found."
|
||||
case .connectionFailed(let msg) where msg.contains("errno: 110"):
|
||||
return "Connection timed out. Check server address and network."
|
||||
case .connectionFailed(let msg) where msg.contains("errno: 111"):
|
||||
return "Cannot reach server. Check server address."
|
||||
case .listingFailed(let msg) where msg.contains("errno: 13"):
|
||||
return "Access denied to this folder."
|
||||
default:
|
||||
return errorDescription ?? "Unknown error"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// SMB protocol version for connection preferences (Swift wrapper).
|
||||
enum SMBProtocol: Int32, Codable, Hashable, Sendable, CaseIterable {
|
||||
case auto = 0
|
||||
case smb1 = 1
|
||||
case smb2 = 2
|
||||
case smb3 = 3
|
||||
|
||||
/// Display name for UI
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .auto: return String(localized: "smb.protocol.auto")
|
||||
case .smb1: return "SMB1"
|
||||
case .smb2: return "SMB2"
|
||||
case .smb3: return "SMB3"
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to C enum type
|
||||
var cValue: SMBProtocolVersion {
|
||||
SMBProtocolVersion(UInt32(rawValue))
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-safe wrapper around libsmbclient context.
|
||||
actor SMBBridgeContext {
|
||||
private var context: UnsafeMutableRawPointer?
|
||||
private let workgroup: String
|
||||
private let username: String?
|
||||
private let password: String?
|
||||
private let protocolVersion: SMBProtocol
|
||||
|
||||
init(workgroup: String = "WORKGROUP",
|
||||
username: String?,
|
||||
password: String?,
|
||||
protocolVersion: SMBProtocol = SMBProtocol.auto) {
|
||||
self.workgroup = workgroup
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.protocolVersion = protocolVersion
|
||||
}
|
||||
|
||||
/// Initialize the SMB context.
|
||||
func initialize() throws {
|
||||
guard context == nil else { return }
|
||||
|
||||
LoggingService.shared.logMediaSourcesDebug("Initializing SMB context with workgroup: \(self.workgroup), protocol: \(self.protocolVersion.rawValue)")
|
||||
|
||||
let wg = workgroup.cString(using: .utf8)
|
||||
let user = username?.cString(using: .utf8)
|
||||
let pass = password?.cString(using: .utf8)
|
||||
|
||||
// Use the Swift enum's conversion to C enum type
|
||||
context = smb_init_context(wg, user, pass, protocolVersion.cValue)
|
||||
|
||||
if context == nil {
|
||||
LoggingService.shared.logMediaSourcesError("Failed to initialize SMB context")
|
||||
throw SMBBridgeError.contextInitFailed
|
||||
}
|
||||
|
||||
LoggingService.shared.logMediaSources("SMB context initialized successfully")
|
||||
}
|
||||
|
||||
/// List directory contents at given SMB URL.
|
||||
func listDirectory(at url: String) throws -> [SMBFileEntry] {
|
||||
guard let context = context else {
|
||||
throw SMBBridgeError.contextInitFailed
|
||||
}
|
||||
|
||||
LoggingService.shared.logMediaSourcesDebug("Listing SMB directory: \(url)")
|
||||
|
||||
var count: Int32 = 0
|
||||
var errorPtr: UnsafeMutablePointer<CChar>?
|
||||
|
||||
guard let fileList = smb_list_directory(context, url, &count, &errorPtr) else {
|
||||
let errorMsg = errorPtr.map { String(cString: $0) } ?? "Unknown error"
|
||||
if let errorPtr = errorPtr {
|
||||
free(errorPtr)
|
||||
}
|
||||
|
||||
// Empty directory is not an error
|
||||
if count == 0 && errorMsg == "Unknown error" {
|
||||
LoggingService.shared.logMediaSourcesDebug("Directory is empty")
|
||||
return []
|
||||
}
|
||||
|
||||
LoggingService.shared.logMediaSourcesError("Failed to list directory: \(errorMsg)")
|
||||
throw SMBBridgeError.listingFailed(errorMsg)
|
||||
}
|
||||
|
||||
defer { smb_free_file_list(fileList, count) }
|
||||
|
||||
var entries: [SMBFileEntry] = []
|
||||
|
||||
for i in 0..<Int(count) {
|
||||
let fileInfo = fileList[i]
|
||||
let name = String(cString: fileInfo.name)
|
||||
|
||||
// SMBC_FILE_SHARE = 3, SMBC_DIR = 7, SMBC_FILE = 8 (from libsmbclient.h)
|
||||
let isShare = fileInfo.type == 3
|
||||
let isDirectory = fileInfo.type == 7
|
||||
|
||||
let modifiedDate = fileInfo.mtime > 0
|
||||
? Date(timeIntervalSince1970: TimeInterval(fileInfo.mtime))
|
||||
: nil
|
||||
let createdDate = fileInfo.ctime > 0
|
||||
? Date(timeIntervalSince1970: TimeInterval(fileInfo.ctime))
|
||||
: nil
|
||||
|
||||
entries.append(SMBFileEntry(
|
||||
name: name,
|
||||
isDirectory: isDirectory,
|
||||
isShare: isShare,
|
||||
size: Int64(fileInfo.size),
|
||||
modifiedDate: modifiedDate,
|
||||
createdDate: createdDate
|
||||
))
|
||||
}
|
||||
|
||||
LoggingService.shared.logMediaSources("Listed \(entries.count) items from SMB directory")
|
||||
return entries
|
||||
}
|
||||
|
||||
/// Test connection to SMB URL.
|
||||
func testConnection(to url: String) throws {
|
||||
guard let context = context else {
|
||||
throw SMBBridgeError.contextInitFailed
|
||||
}
|
||||
|
||||
LoggingService.shared.logMediaSourcesDebug("Testing SMB connection to: \(url)")
|
||||
|
||||
let result = smb_test_connection(context, url)
|
||||
if result != 0 {
|
||||
let errorMsg = "Connection test failed with error code: \(result)"
|
||||
LoggingService.shared.logMediaSourcesError(errorMsg)
|
||||
throw SMBBridgeError.connectionFailed(errorMsg)
|
||||
}
|
||||
|
||||
LoggingService.shared.logMediaSources("SMB connection test succeeded")
|
||||
}
|
||||
|
||||
/// Download file from SMB to local path.
|
||||
func downloadFile(from url: String, to localPath: String) throws {
|
||||
guard let context = context else {
|
||||
throw SMBBridgeError.contextInitFailed
|
||||
}
|
||||
|
||||
LoggingService.shared.logMediaSourcesDebug("Downloading file from: \(url)")
|
||||
LoggingService.shared.logMediaSourcesDebug("Downloading file to: \(localPath)")
|
||||
|
||||
var errorPtr: UnsafeMutablePointer<CChar>?
|
||||
let result = smb_download_file(context, url, localPath, &errorPtr)
|
||||
|
||||
if result != 0 {
|
||||
let errorMsg = errorPtr.map { String(cString: $0) } ?? "Unknown error"
|
||||
if let errorPtr = errorPtr {
|
||||
free(errorPtr)
|
||||
}
|
||||
LoggingService.shared.logMediaSourcesError("Failed to download file: \(errorMsg)")
|
||||
throw SMBBridgeError.connectionFailed(errorMsg)
|
||||
}
|
||||
|
||||
LoggingService.shared.logMediaSources("File download succeeded")
|
||||
}
|
||||
|
||||
/// Clean up resources.
|
||||
deinit {
|
||||
if let context = context {
|
||||
smb_free_context(context)
|
||||
LoggingService.shared.logMediaSourcesDebug("SMB context cleaned up")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// libsmbclient_minimal.h
|
||||
// Yattee
|
||||
//
|
||||
// Minimal forward declarations for libsmbclient context-based API to avoid header dependency issues.
|
||||
// This uses the modern context API exported by MPVKit-GPL's Libsmbclient.framework.
|
||||
//
|
||||
|
||||
#ifndef libsmbclient_minimal_h
|
||||
#define libsmbclient_minimal_h
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
|
||||
// SMB entry types (from libsmbclient.h)
|
||||
#define SMBC_WORKGROUP 1
|
||||
#define SMBC_SERVER 2
|
||||
#define SMBC_FILE_SHARE 3
|
||||
#define SMBC_PRINTER_SHARE 4
|
||||
#define SMBC_COMMS_SHARE 5
|
||||
#define SMBC_IPC_SHARE 6
|
||||
#define SMBC_DIR 7
|
||||
#define SMBC_FILE 8
|
||||
#define SMBC_LINK 9
|
||||
|
||||
// Forward declarations for context types
|
||||
typedef struct _SMBCCTX SMBCCTX;
|
||||
typedef struct _SMBCFILE SMBCFILE;
|
||||
|
||||
// Context-specific function pointer types (for true context isolation)
|
||||
// These allow calling SMB functions on a specific context without using smbc_set_context()
|
||||
typedef SMBCFILE * (*smbc_opendir_fn)(SMBCCTX *c, const char *fname);
|
||||
typedef int (*smbc_closedir_fn)(SMBCCTX *c, SMBCFILE *dir);
|
||||
typedef struct smbc_dirent * (*smbc_readdir_fn)(SMBCCTX *c, SMBCFILE *dir);
|
||||
typedef off_t (*smbc_lseekdir_fn)(SMBCCTX *c, SMBCFILE *dir, off_t offset);
|
||||
typedef int (*smbc_stat_fn)(SMBCCTX *c, const char *fname, struct stat *st);
|
||||
typedef SMBCFILE * (*smbc_open_fn)(SMBCCTX *c, const char *fname, int flags, mode_t mode);
|
||||
typedef ssize_t (*smbc_read_fn)(SMBCCTX *c, SMBCFILE *file, void *buf, size_t count);
|
||||
typedef int (*smbc_close_fn)(SMBCCTX *c, SMBCFILE *file);
|
||||
|
||||
// Directory entry structure (from libsmbclient.h)
|
||||
struct smbc_dirent {
|
||||
unsigned int smbc_type;
|
||||
unsigned int dirlen;
|
||||
unsigned int commentlen;
|
||||
char *comment;
|
||||
unsigned int namelen;
|
||||
char name[1]; // Variable length
|
||||
};
|
||||
|
||||
// Auth callback type with context (modern API)
|
||||
typedef void (*smbc_get_auth_data_with_context_fn)(
|
||||
SMBCCTX *ctx,
|
||||
const char *server, const char *share,
|
||||
char *workgroup, int wgmaxlen,
|
||||
char *username, int unmaxlen,
|
||||
char *password, int pwmaxlen
|
||||
);
|
||||
|
||||
// Context management (modern context-based API)
|
||||
extern SMBCCTX *smbc_new_context(void);
|
||||
extern SMBCCTX *smbc_init_context(SMBCCTX *ctx);
|
||||
extern int smbc_free_context(SMBCCTX *ctx, int shutdown_ctx);
|
||||
|
||||
// Context configuration functions
|
||||
extern void smbc_setFunctionAuthDataWithContext(SMBCCTX *ctx, smbc_get_auth_data_with_context_fn fn);
|
||||
extern void smbc_setOptionUserData(SMBCCTX *ctx, void *user_data);
|
||||
extern void *smbc_getOptionUserData(SMBCCTX *ctx);
|
||||
extern void smbc_setTimeout(SMBCCTX *ctx, int timeout);
|
||||
extern void smbc_setWorkgroup(SMBCCTX *ctx, const char *workgroup);
|
||||
extern void smbc_setUser(SMBCCTX *ctx, const char *user);
|
||||
|
||||
// Context-specific function pointer getters (preferred API for multi-context usage)
|
||||
// These provide true context isolation without affecting global state
|
||||
extern smbc_opendir_fn smbc_getFunctionOpendir(SMBCCTX *c);
|
||||
extern smbc_closedir_fn smbc_getFunctionClosedir(SMBCCTX *c);
|
||||
extern smbc_readdir_fn smbc_getFunctionReaddir(SMBCCTX *c);
|
||||
extern smbc_lseekdir_fn smbc_getFunctionLseekdir(SMBCCTX *c);
|
||||
extern smbc_stat_fn smbc_getFunctionStat(SMBCCTX *c);
|
||||
extern smbc_open_fn smbc_getFunctionOpen(SMBCCTX *c);
|
||||
extern smbc_read_fn smbc_getFunctionRead(SMBCCTX *c);
|
||||
extern smbc_close_fn smbc_getFunctionClose(SMBCCTX *c);
|
||||
|
||||
// Boolean type for libsmbclient
|
||||
typedef int smbc_bool;
|
||||
|
||||
// Set SMB protocol version (min/max)
|
||||
extern smbc_bool smbc_setOptionProtocols(SMBCCTX *c, const char *min_proto, const char *max_proto);
|
||||
|
||||
#endif /* libsmbclient_minimal_h */
|
||||
Reference in New Issue
Block a user