Files
crm.clientright.ru/crm_extensions/file_storage/NextcloudClient.php
Fedor 76abcbc70b fix: ПРАВИЛЬНАЯ миграция путей Nextcloud /crm2/ → /crm/crm2/
ИСПРАВЛЕНИЕ: На новом Nextcloud S3 примонтирован как /crm, но внутри папка crm2
Правильный путь: /crm/crm2/CRM_Active_Files/

Выполнено в БД (rollback_and_fix.sql):
- Откат предыдущей неправильной миграции из backup
- s3_key: crm2/ → crm/crm2/ (17386 записей)
- nc_path: /crm2/crm2/ → /crm/crm2/ (72 записи)
- nc_path: /crm2/ → /crm/crm2/ (все остальные)

Обновлены файлы кода:
- crm_extensions/file_storage/config.php: active_folder = /crm/crm2/CRM_Active_Files/
- crm_extensions/nextcloud_editor/js/nextcloud-editor.js (5 путей)
- crm_extensions/file_storage/api/get_edit_urls.php (6 путей)
- modules/Documents/actions/NcPrepareEdit.php (2 пути)
- crm_extensions/file_storage/api/prepare_edit.php (1 путь)
- crm_extensions/file_storage/NextcloudClient.php (1 путь)

Документ 395695:
- s3_key: crm/crm2/CRM_Active_Files/Documents/395695/zayavlenie_proekt.docx ✓
- nc_path: /crm/crm2/CRM_Active_Files/Documents/395695/zayavlenie_proekt.docx ✓
2025-10-20 18:54:33 +03:00

368 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Nextcloud WebDAV Client (Fixed)
* Клиент для работы с Nextcloud через WebDAV API
*/
class NextcloudClient {
private $baseUrl;
private $username;
private $password;
private $activeFolder;
public function __construct($config) {
$this->baseUrl = rtrim($config['base_url'], '/');
$this->username = $config['username'];
$this->password = $config['password'];
$this->activeFolder = trim($config['active_folder'], '/');
}
/**
* Загрузка файла в Nextcloud
*/
public function uploadFile($localPath, $remotePath) {
try {
// Убираем activeFolder из remotePath если он уже там есть
if (strpos($remotePath, $this->activeFolder) === 0) {
$fullRemotePath = $remotePath;
} else {
$fullRemotePath = $this->activeFolder . '/' . ltrim($remotePath, '/');
}
$url = $this->baseUrl . '/remote.php/dav/files/' . $this->username . '/' . $fullRemotePath;
echo "Uploading to URL: $url\n";
// Создаём папку если не существует
$this->ensureFolderExists(dirname($fullRemotePath));
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_INFILE => fopen($localPath, 'r'),
CURLOPT_INFILESIZE => filesize($localPath),
CURLOPT_USERPWD => $this->username . ':' . $this->password,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
CURLOPT_HTTPHEADER => [
'Content-Type: ' . $this->getMimeType($localPath)
]
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
throw new Exception('Curl error: ' . $error);
}
if ($httpCode < 200 || $httpCode >= 300) {
throw new Exception('HTTP error: ' . $httpCode . ' - ' . $response);
}
return [
'success' => true,
'remote_path' => $fullRemotePath,
'url' => $url
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Создание папки (публичный метод)
*/
public function createFolder($folderPath) {
try {
$this->ensureFolderExists($folderPath);
return [
'success' => true,
'folder_path' => $folderPath
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Создание папки (улучшенная версия)
*/
private function ensureFolderExists($folderPath) {
if (empty($folderPath) || $folderPath === '.') {
return;
}
// Разбиваем путь на части
$parts = explode('/', trim($folderPath, '/'));
$currentPath = '';
foreach ($parts as $part) {
if (empty($part)) continue;
$currentPath .= '/' . $part;
$url = $this->baseUrl . '/remote.php/dav/files/' . $this->username . $currentPath;
echo "Creating folder: $url\n";
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_CUSTOMREQUEST => 'MKCOL',
CURLOPT_USERPWD => $this->username . ':' . $this->password,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
echo "Folder creation result: HTTP $httpCode\n";
// 201 = created, 405 = already exists, оба OK
if ($httpCode !== 201 && $httpCode !== 405) {
echo "Response: $response\n";
if ($error) {
throw new Exception("Curl error creating folder '$currentPath': $error");
} else {
throw new Exception("HTTP error creating folder '$currentPath': $httpCode - $response");
}
}
}
}
/**
* Получение ID файла из Nextcloud
*/
public function getFileId($remotePath) {
try {
$fullRemotePath = $this->activeFolder . '/' . ltrim($remotePath, '/');
$url = $this->baseUrl . '/remote.php/dav/files/' . $this->username . '/' . $fullRemotePath;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_CUSTOMREQUEST => 'PROPFIND',
CURLOPT_USERPWD => $this->username . ':' . $this->password,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Content-Type: application/xml',
'Depth: 0'
],
CURLOPT_POSTFIELDS => '<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:prop>
<oc:fileid/>
</d:prop>
</d:propfind>'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 207 && $response) {
$xml = simplexml_load_string($response);
if ($xml) {
foreach ($xml->xpath('//oc:fileid') as $element) {
return (string)$element;
}
}
}
return null;
} catch (Exception $e) {
error_log("Error getting file ID: " . $e->getMessage());
return null;
}
}
/**
* Создание прямой ссылки для редактирования файла
*/
public function createDirectEditLink($remotePath, $recordId, $fileName = null) {
// Создаем ссылку на файловый менеджер Nextcloud с открытием файла
if ($fileName === null) {
$fileName = basename($remotePath);
}
// Убираем подчеркивание в начале, если есть
if (substr($fileName, 0, 1) === '_') {
$fileName = substr($fileName, 1);
}
$editUrl = $this->baseUrl . '/apps/files/?dir=/crm/crm2/CRM_Active_Files/Documents/' . $recordId . '&openfile=' . urlencode($fileName);
return [
'success' => true,
'edit_url' => $editUrl,
'direct_url' => $editUrl,
'file_id' => null
];
}
/**
* Создание ссылки для редактирования файла
*/
public function createEditLink($remotePath, $permissions = 3) {
try {
$fullRemotePath = $this->activeFolder . '/' . ltrim($remotePath, '/');
// Создаём публичную ссылку через OCS API
$url = $this->baseUrl . '/ocs/v2.php/apps/files_sharing/api/v1/shares';
$postData = http_build_query([
'path' => '/' . $fullRemotePath,
'shareType' => 3, // Public link
'permissions' => $permissions, // 1=read, 2=update, 3=read+update
'format' => 'json'
]);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_USERPWD => $this->username . ':' . $this->password,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'OCS-APIRequest: true',
'Content-Type: application/x-www-form-urlencoded'
]
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new Exception('Failed to create share link: HTTP ' . $httpCode . ' - ' . $response);
}
$data = json_decode($response, true);
if (!$data || $data['ocs']['meta']['statuscode'] !== 200) {
$message = $data['ocs']['meta']['message'] ?? 'Unknown OCS error';
throw new Exception('OCS error: ' . $message);
}
$shareUrl = $data['ocs']['data']['url'];
// Для редактирования добавляем параметр
$editUrl = $shareUrl . '/edit';
return [
'success' => true,
'share_url' => $shareUrl,
'edit_url' => $editUrl,
'share_id' => $data['ocs']['data']['id']
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* Проверка существования файла
*/
public function fileExists($remotePath) {
$fullRemotePath = $this->activeFolder . '/' . ltrim($remotePath, '/');
$url = $this->baseUrl . '/remote.php/dav/files/' . $this->username . '/' . $fullRemotePath;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_CUSTOMREQUEST => 'HEAD',
CURLOPT_USERPWD => $this->username . ':' . $this->password,
CURLOPT_NOBODY => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_RETURNTRANSFER => true
]);
curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $httpCode === 200;
}
/**
* Скачивание файла из Nextcloud
*/
public function downloadToTemp($remotePath) {
$fullRemotePath = $this->activeFolder . '/' . ltrim($remotePath, '/');
$url = $this->baseUrl . '/remote.php/dav/files/' . $this->username . '/' . $fullRemotePath;
$tempFile = sys_get_temp_dir() . '/' . uniqid('nextcloud_download_') . '_' . basename($remotePath);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_USERPWD => $this->username . ':' . $this->password,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120
]);
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new Exception('Download failed: HTTP ' . $httpCode);
}
file_put_contents($tempFile, $result);
return $tempFile;
}
/**
* Удаление файла
*/
public function deleteFile($remotePath) {
$fullRemotePath = $this->activeFolder . '/' . ltrim($remotePath, '/');
$url = $this->baseUrl . '/remote.php/dav/files/' . $this->username . '/' . $fullRemotePath;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_USERPWD => $this->username . ':' . $this->password,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $httpCode === 204; // 204 = No Content (успешное удаление)
}
/**
* Получение MIME типа файла
*/
private function getMimeType($filePath) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $filePath);
finfo_close($finfo);
return $mimeType ?: 'application/octet-stream';
}
}