- 🔍 1. Общая информация
- 🔐 2. Аутентификация и авторизация
- 📤 3. Базовые примеры загрузки файлов
- 🚀 4. Расширенные возможности
- ⚠️ 5. Обработка ответов и ошибок
- 📱 6. Особенности работы с данным API из Android приложений
- ⚙️ 7. Тонкая настройка и редкие случаи
- 📋 8. Приложения
- 🎯 Заключение
🔍 1. Общая информация
1.1 Назначение эндпоинта
/api/v2/upload.php — специализированный эндпоинт для асинхронной загрузки файлов в систему BatteryWizard. Поддерживает комплексные сценарии загрузки, включая основной файл данных и сопутствующие изображения с привязкой к типам контента.
1.2 Основные возможности
- 📤 Загрузка основного файла (форматы: CBE, RTA, BSL, DAT, и другие)
- 🖼️ Сопутствующие изображения (JPG, PNG, GIF, WebP, BMP) с привязкой к ContentType
- 🔒 Многоуровневая безопасность (IP-фильтрация, rate-limiting, проверка контента)
- 📊 Детальное логирование и отладка в test-режиме
- 💾 Автоматическое разрешение коллизий имён файлов
- 🔍 Валидация содержимого (сигнатуры файлов, MIME-типы)
- 🔄 Транзакционная обработка (atomic-операции с rollback)
1.3 Архитектурные ограничения
Максимальный размер основного файла: 100 MB (конфигурируется)
Максимальный размер изображения: 10 MB (конфигурируется)
Минимальный размер файла: 1 байт
Требуется один основной файл: Обязательно ровно 1
Количество изображений: От 0 до N (ограничено лимитами пользователя)
🔐 2. Аутентификация и авторизация
2.1 Методы передачи токена
Эндпоинт поддерживает три способа передачи JWT-токена (в порядке приоритета):
2.1.1 HTTP Header (рекомендуемый)
Authorization: Bearer bdf380fc8f1234567890abcdef123456
2.1.2 GET/POST параметр
POST /api/v2/upload.php?token=bdf380fc8f1234567890abcdef123456
2.1.3 Multipart Form Data
<input type="hidden" name="token" value="bdf380fc8f1234567890abcdef123456">
2.2 Лимиты пользователя
Каждый пользователь имеет индивидуальные лимиты:
| Лимит | Значение по умолчанию | Описание |
|---|---|---|
| Daily Upload Limit | 500 файлов | Максимальное количество загрузок в сутки |
| Trust Score | 100 (макс) | Динамическая оценка доверия |
| AllowBan | 1 (разрешено) | Возможность блокировки системы безопасности |
Важно: При достижении лимита возвращается HTTP 429 с сообщением «Daily upload limit exceeded».
📤 3. Базовые примеры загрузки файлов
3.1 Простейший запрос (один файл)
cURL
curl -X POST "https://api.batterywizard.ru/v2/upload.php" \
-H "Authorization: Bearer bdf380fc8f1234567890abcdef123456" \
-F "upload_file=@test_file.rta" \
-F "filename=test_file.rta" \
-F "version=1.0"
Пример успешного ответа (HTTP 200)
{
"success": true,
"code": 200,
"message": "Files uploaded successfully",
"timestamp": "2025-12-09T10:30:00+03:00",
"request_id": "2d5e2745398c5671",
"data": {
"main_file": {
"id": 15372,
"name": "test_file.rta",
"size": 40000,
"path": "/upload/test_user/RTA/test_file_1764872586_1.rta",
"crc32": 818415889
},
"images": [],
"summary": {
"total_files": 1,
"total_size": 40000,
"image_count": 0
}
}
}
3.2 Загрузка с изображением
HTML Form
<form action="https://api.batterywizard.ru/v2/upload.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="token" value="bdf380fc8f1234567890abcdef123456">
<!-- Основной файл -->
<input type="file" name="upload_file">
<input type="text" name="filename" value="test_analysis.rta">
<!-- Изображение -->
<input type="file" name="upload_photo">
<input type="number" name="upload_photo_id" value="5">
<!-- Метаданные -->
<input type="text" name="version" value="2.1">
<input type="text" name="uuid" value="device-12345">
<input type="text" name="os" value="Android 14">
<input type="text" name="carrier" value="MegaFon">
<button type="submit">Upload</button>
</form>
Пример ответа с изображением
{
"success": true,
"code": 200,
"message": "Files uploaded successfully",
"timestamp": "2025-12-09T10:32:15+03:00",
"request_id": "927760349a58c681",
"data": {
"main_file": {
"id": 15373,
"name": "test_analysis.rta",
"size": 30500,
"path": "/upload/test_user/RTA/test_analysis_1764873135_1.rta",
"crc32": 2948573621
},
"images": [
{
"id": 16427,
"name": "photo1.jpg",
"size": 2958,
"content_type_id": 5
}
],
"summary": {
"total_files": 2,
"total_size": 33458,
"image_count": 1
}
}
}
🚀 4. Расширенные возможности
4.1 Множественные изображения с разными ContentType
Структура запроса
POST /api/v2/upload.php HTTP/1.1
Authorization: Bearer bdf380fc8f1234567890abcdef123456
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary123456
------WebKitFormBoundary123456
Content-Disposition: form-data; name="upload_file"; filename="analysis.cbe"
Content-Type: application/octet-stream
[бинарные данные файла]
------WebKitFormBoundary123456
Content-Disposition: form-data; name="upload_photo[]"; filename="overview.jpg"
Content-Type: image/jpeg
[бинарные данные изображения]
------WebKitFormBoundary123456
Content-Disposition: form-data; name="upload_photo[]"; filename="details.jpg"
Content-Type: image/jpeg
[бинарные данные изображения]
------WebKitFormBoundary123456
Content-Disposition: form-data; name="upload_photo_id[0]"
Content-Type: text/plain
3
------WebKitFormBoundary123456
Content-Disposition: form-data; name="upload_photo_id[1]"
Content-Type: text/plain
7
------WebKitFormBoundary123456
Content-Disposition: form-data; name="filename"
Content-Type: text/plain
full_analysis.cbe
Примечание: Индексы в upload_photo_id[] соответствуют порядку файлов в upload_photo[].
4.2 Base64 кодирование файлов
Для случаев, когда multipart недоступен:
POST /api/v2/upload.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
token=bdf380fc8f1234567890abcdef123456&
file=[BASE64_ENCODED_FILE_CONTENT]&
filename=encoded_file.dat&
version=1.2
4.3 Специфические ContentType ID
Система поддерживает привязку изображений к типам контента через таблицу Content_Type:
| ID | Описание | Типичное использование |
|---|---|---|
| 1 | Общий вид | Фото батареи целиком |
| 2 | Клеммы | Контакты и соединения |
| 3 | Маркировка | Серийные номера, штрих-коды |
| 4 | Дефекты | Повреждения, коррозия |
| 5 | Графики | Визуализации данных |
| 6 | Схемы | Электрические схемы |
| 7 | Документы | Сопутствующая документация |
Важно: Если таблица Content_Type не существует или ID не найден, используется значение 0 (без типа).
⚠️ 5. Обработка ответов и ошибок
5.1 Структура успешного ответа
{
"success": true,
"code": 200,
"message": "Files uploaded successfully",
"timestamp": "2025-12-09T10:30:00+03:00",
"request_id": "2d5e2745398c5671",
"data": {
"main_file": {
"id": 15372,
"name": "test_file.rta",
"size": 40000,
"path": "/upload/test_user/RTA/test_file_1764872586_1.rta",
"crc32": 818415889
},
"images": [
{
"id": 16425,
"name": "photo.jpg",
"size": 2958,
"content_type_id": 5
}
],
"summary": {
"total_files": 2,
"total_size": 42958,
"image_count": 1
}
}
}
5.2 Коды ошибок и их обработка
5.2.1 HTTP 400 — Ошибки клиента
{
"success": false,
"code": 400,
"message": "Exactly one main (non-image) file is required",
"timestamp": "2025-12-09T10:31:22+03:00",
"request_id": "33a2726b3f31bf9b"
}
Типичные причины:
- Не загружены файлы
- Неправильное количество основных файлов (не 1)
- Пустые файлы или слишком маленький размер
- Неподдерживаемое расширение файла
5.2.2 HTTP 401/403 — Ошибки аутентификации
{
"success": false,
"code": 403,
"message": "Invalid or expired authentication token",
"timestamp": "2025-12-09T10:31:45+03:00",
"request_id": "49af228aff703066"
}
5.2.3 HTTP 413 — Превышен размер файла
{
"success": false,
"code": 413,
"message": "File 'large_video.mp4' exceeds maximum size of 100.0 MB",
"timestamp": "2025-12-09T10:32:10+03:00",
"request_id": "63f072d171d2432e"
}
5.2.4 HTTP 429 — Превышены лимиты
{
"success": false,
"code": 429,
"message": "Daily upload limit exceeded",
"timestamp": "2025-12-09T10:33:01+03:00",
"request_id": "400ea9aa4b3ce1a0"
}
5.2.5 HTTP 500 — Внутренние ошибки сервера
{
"success": false,
"code": 500,
"message": "Internal server error",
"timestamp": "2025-12-09T10:33:30+03:00",
"request_id": "970dbd275b1f78ae",
"error_details": "Data too long for column 'SystemPath' at row 1"
}
Примечание: Детали ошибки возвращаются только в тестовом режиме или при включенном debug.
5.3 Test Mode и отладка
Активация тестового режима
POST /api/v2/upload.php
X-Test-Session: test_session_6931d1844125c4.29687211
Расширенный ответ в тестовом режиме
{
"success": true,
"code": 200,
"message": "Files uploaded successfully",
"timestamp": "2025-12-09T10:34:00+03:00",
"request_id": "927760349a58c681",
"data": { ... },
"debug": {
"test_mode": true,
"test_session": "test_session_6931d1844125c4.29687211",
"debug_log_file": "upload_debug_test_session_6931d1844125c4.29687211_2025-12-04_21-23-06.log",
"user_id": 391,
"username": "test_v2",
"company_id": 3,
"memory_usage": 2097152,
"peak_memory": 2097152,
"debug_entries": 40
}
}
Логи тестового режима сохраняются в: log/debug/upload_debug_{SESSION_ID}_{TIMESTAMP}.log
📱 6. Особенности работы с данным API из Android приложений
6.1 Рекомендуемые библиотеки
Для HTTP-клиента
// Retrofit + OkHttp (рекомендуется)
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
Для работы с Multipart
// Apache Commons IO для упрощения работы с файлами
implementation 'commons-io:commons-io:2.15.1'
6.2 Реализация загрузки на Kotlin
6.2.1 Базовая конфигурация Retrofit
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object ApiClient {
private const val BASE_URL = "https://api.batterywizard.ru/v2/"
private const val TIMEOUT = 300L // Увеличено для загрузки файлов
fun create(): BatteryWizardApi {
val logging = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
val client = OkHttpClient.Builder()
.connectTimeout(TIMEOUT, TimeUnit.SECONDS)
.readTimeout(TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(TIMEOUT, TimeUnit.SECONDS)
.addInterceptor(logging)
.addInterceptor { chain ->
val original = chain.request()
val request = original.newBuilder()
.header("User-Agent", "BatteryWizardAndroid/1.0")
.header("Accept", "application/json")
.build()
chain.proceed(request)
}
.build()
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(BatteryWizardApi::class.java)
}
}
6.2.2 Определение интерфейса API
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.http.*
interface BatteryWizardApi {
@Multipart
@POST("upload.php")
fun uploadFile(
@Header("Authorization") auth: String,
@Part("filename") filename: RequestBody,
@Part("version") version: RequestBody,
@Part("uuid") uuid: RequestBody,
@Part("os") os: RequestBody,
@Part("carrier") carrier: RequestBody,
@Part uploadFile: MultipartBody.Part,
@Part photos: List<MultipartBody.Part>? = null,
@Part("upload_photo_id") photoIds: Map<String, RequestBody>? = null
): Call<UploadResponse>
@Multipart
@POST("upload.php")
fun uploadFileWithContentType(
@Header("Authorization") auth: String,
@Header("X-Test-Session") testSession: String? = null,
@Part("filename") filename: RequestBody,
@Part uploadFile: MultipartBody.Part,
@Part photos: List<MultipartBody.Part>? = null,
@PartMap contentTypes: Map<String, RequestBody>? = null
): Call<UploadResponse>
}
6.2.3 Модели данных
import com.google.gson.annotations.SerializedName
data class UploadResponse(
@SerializedName("success") val success: Boolean,
@SerializedName("code") val code: Int,
@SerializedName("message") val message: String,
@SerializedName("timestamp") val timestamp: String,
@SerializedName("request_id") val requestId: String,
@SerializedName("data") val data: UploadData? = null,
@SerializedName("debug") val debug: DebugInfo? = null
)
data class UploadData(
@SerializedName("main_file") val mainFile: MainFile,
@SerializedName("images") val images: List<ImageInfo>,
@SerializedName("summary") val summary: UploadSummary
)
data class MainFile(
@SerializedName("id") val id: Long,
@SerializedName("name") val name: String,
@SerializedName("size") val size: Long,
@SerializedName("path") val path: String,
@SerializedName("crc32") val crc32: Long
)
data class ImageInfo(
@SerializedName("id") val id: Long,
@SerializedName("name") val name: String,
@SerializedName("size") val size: Long,
@SerializedName("content_type_id") val contentTypeId: Int
)
data class UploadSummary(
@SerializedName("total_files") val totalFiles: Int,
@SerializedName("total_size") val totalSize: Long,
@SerializedName("image_count") val imageCount: Int
)
data class DebugInfo(
@SerializedName("test_mode") val testMode: Boolean,
@SerializedName("test_session") val testSession: String,
@SerializedName("debug_log_file") val debugLogFile: String,
@SerializedName("user_id") val userId: Int,
@SerializedName("username") val username: String,
@SerializedName("company_id") val companyId: Int,
@SerializedName("memory_usage") val memoryUsage: Long,
@SerializedName("peak_memory") val peakMemory: Long,
@SerializedName("debug_entries") val debugEntries: Int
)
6.2.4 Рабочий пример загрузки
import android.net.Uri
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
class FileUploader(private val context: Context) {
suspend fun uploadBatteryData(
authToken: String,
mainFileUri: Uri,
imageUris: List<Uri>? = null,
contentTypeIds: List<Int>? = null
): Result<UploadResponse> = withContext(Dispatchers.IO) {
return@withContext try {
// Подготовка основного файла
val mainFile = File(mainFileUri.path!!)
val mainFilePart = MultipartBody.Part.createFormData(
"upload_file",
mainFile.name,
mainFile.asRequestBody("application/octet-stream".toMediaTypeOrNull())
)
// Подготовка изображений
val photoParts = mutableListOf<MultipartBody.Part>()
val photoIds = mutableMapOf<String, RequestBody>()
imageUris?.forEachIndexed { index, uri ->
val imageFile = File(uri.path!!)
val photoPart = MultipartBody.Part.createFormData(
"upload_photo",
imageFile.name,
imageFile.asRequestBody("image/jpeg".toMediaTypeOrNull())
)
photoParts.add(photoPart)
// Привязка ContentType ID
contentTypeIds?.getOrNull(index)?.let { contentTypeId ->
photoIds["upload_photo_id[$index]"] =
contentTypeId.toString().toRequestBody(MultipartBody.FORM)
}
}
// Метаданные
val filename = mainFile.name.toRequestBody(MultipartBody.FORM)
val version = "2.0".toRequestBody(MultipartBody.FORM)
val uuid = Build.MODEL.toRequestBody(MultipartBody.FORM)
val os = "Android ${Build.VERSION.RELEASE}".toRequestBody(MultipartBody.FORM)
val carrier = getCarrierName().toRequestBody(MultipartBody.FORM)
// Вызов API
val response = ApiClient.create().uploadFile(
auth = "Bearer $authToken",
filename = filename,
version = version,
uuid = uuid,
os = os,
carrier = carrier,
uploadFile = mainFilePart,
photos = if (photoParts.isNotEmpty()) photoParts else null,
photoIds = if (photoIds.isNotEmpty()) photoIds else null
).execute()
if (response.isSuccessful) {
Result.success(response.body()!!)
} else {
val errorBody = response.errorBody()?.string()
Result.failure(Exception("Upload failed: ${response.code()} - $errorBody"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
private fun getCarrierName(): String {
return try {
val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
tm.networkOperatorName.takeIf { it.isNotBlank() } ?: "Unknown"
} catch (e: Exception) {
"Unknown"
}
}
}
6.3 Обработка больших файлов и прогресс
6.3.1 Кастомный Interceptor для отслеживания прогресса
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
class ProgressInterceptor(
private val progressListener: (bytesWritten: Long, contentLength: Long) -> Unit
) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val originalResponse = chain.proceed(chain.request())
return originalResponse.newBuilder()
.body(originalResponse.body?.let { ProgressResponseBody(it, progressListener) })
.build()
}
}
class ProgressResponseBody(
private val responseBody: ResponseBody,
private val progressListener: (Long, Long) -> Unit
) : ResponseBody() {
private var bufferedSource: BufferedSource? = null
override fun contentType(): MediaType? = responseBody.contentType()
override fun contentLength(): Long = responseBody.contentLength()
override fun source(): BufferedSource {
if (bufferedSource == null) {
bufferedSource = source(responseBody.source()).buffer()
}
return bufferedSource!!
}
private fun source(source: Source): Source {
return object : ForwardingSource(source) {
var totalBytesRead = 0L
@Throws(IOException::class)
override fun read(sink: Buffer, byteCount: Long): Long {
val bytesRead = super.read(sink, byteCount)
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
progressListener(totalBytesRead, responseBody.contentLength())
return bytesRead
}
}
}
}
6.3.2 Использование в Activity/Fragment
class UploadActivity : AppCompatActivity() {
private lateinit var binding: ActivityUploadBinding
private val viewModel: UploadViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityUploadBinding.inflate(layoutInflater)
setContentView(binding.root)
setupObservers()
}
private fun setupObservers() {
viewModel.uploadProgress.observe(this) { progress ->
binding.progressBar.progress = progress
binding.progressText.text = "$progress%"
}
viewModel.uploadResult.observe(this) { result ->
result.onSuccess { response ->
showSuccess(response)
}.onFailure { error ->
showError(error)
}
}
}
fun startUpload() {
val fileUri = getFileUriFromIntent()
viewModel.uploadFile(fileUri)
}
}
6.4 Особенности Android 10+ (Scoped Storage)
6.4.1 Работа с SAF (Storage Access Framework)
class FilePickerActivity : AppCompatActivity() {
companion object {
private const val REQUEST_PICK_FILE = 101
private const val REQUEST_PICK_IMAGES = 102
}
fun pickMainFile() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf(
"application/octet-stream",
"text/plain",
"application/json"
))
}
startActivityForResult(intent, REQUEST_PICK_FILE)
}
fun pickImages() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/*"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}
startActivityForResult(intent, REQUEST_PICK_IMAGES)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK && data != null) {
when (requestCode) {
REQUEST_PICK_FILE -> {
val uri = data.data
uri?.let { handleFilePicked(it) }
}
REQUEST_PICK_IMAGES -> {
val uris = mutableListOf<Uri>()
if (data.clipData != null) {
val clipData = data.clipData!!
for (i in 0 until clipData.itemCount) {
uris.add(clipData.getItemAt(i).uri)
}
} else {
data.data?.let { uris.add(it) }
}
handleImagesPicked(uris)
}
}
}
}
private fun handleFilePicked(uri: Uri) {
// Получение временного доступа к файлу
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
// Копирование файла в кэш приложения для загрузки
copyFileToCache(uri)
}
}
6.4.2 Копирование файла в кэш
import java.io.File
import java.io.FileOutputStream
private fun copyFileToCache(uri: Uri): File? {
return try {
val inputStream = contentResolver.openInputStream(uri)
val cacheDir = applicationContext.cacheDir
val tempFile = File.createTempFile("upload_", ".tmp", cacheDir)
FileOutputStream(tempFile).use { output ->
inputStream?.copyTo(output)
}
inputStream?.close()
tempFile
} catch (e: Exception) {
Log.e("FileUpload", "Error copying file", e)
null
}
}
6.5 Оптимизация для мобильных сетей
6.5.1 Автоматическое переключение стратегий
class NetworkAwareUploader(
private val context: Context,
private val connectivityManager: ConnectivityManager
) {
fun uploadWithStrategy(file: File, images: List<File>? = null) {
val networkInfo = connectivityManager.activeNetworkInfo
when {
networkInfo?.type == ConnectivityManager.TYPE_WIFI -> {
// Полная загрузка с изображениями
uploadFull(file, images)
}
networkInfo?.type == ConnectivityManager.TYPE_MOBILE -> {
// Только основной файл, изображения отложенно
uploadMainOnly(file)
images?.let { scheduleImagesUpload(it) }
}
else -> {
// Оффлайн - планируем загрузку
scheduleUpload(file, images)
}
}
}
private fun uploadMainOnly(file: File) {
// Упрощенная загрузка только основного файла
// Можно дополнительно сжимать файл перед отправкой
val compressedFile = compressIfNeeded(file)
// ... загрузка
}
private fun compressIfNeeded(file: File): File {
// Логика сжатия файлов для мобильных сетей
return if (file.length() > 5 * 1024 * 1024) { // > 5MB
compressFile(file)
} else {
file
}
}
}
⚙️ 7. Тонкая настройка и редкие случаи
7.1 Кастомные заголовки безопасности
7.1.1 Дополнительные заголовки для корпоративных клиентов
POST /api/v2/upload.php HTTP/1.1
Authorization: Bearer bdf380fc8f1234567890abcdef123456
X-Client-Version: 3.2.1
X-Device-ID: android-1234567890abcdef
X-Session-ID: sess_abc123def456
X-Request-Source: mobile_app
X-Compression: gzip
X-Expected-Size: 10485760
7.2 Resumable Upload (частичная загрузка)
Хотя API напрямую не поддерживает resumable upload, можно реализовать на клиенте:
class ResumableUploader {
private val CHUNK_SIZE = 5 * 1024 * 1024 // 5MB chunks
suspend fun uploadInChunks(file: File, authToken: String): Result<Unit> {
val totalChunks = (file.length() / CHUNK_SIZE).toInt() + 1
var uploadedChunks = loadUploadedChunks(file.name) // Восстановление из SharedPreferences
for (chunkIndex in uploadedChunks until totalChunks) {
val chunk = readChunk(file, chunkIndex, CHUNK_SIZE)
val success = uploadChunk(file.name, chunkIndex, totalChunks, chunk, authToken)
if (success) {
saveUploadedChunk(file.name, chunkIndex)
} else {
return Result.failure(Exception("Failed at chunk $chunkIndex"))
}
}
return Result.success(Unit)
}
private fun readChunk(file: File, chunkIndex: Int, chunkSize: Int): ByteArray {
RandomAccessFile(file, "r").use { raf ->
val position = chunkIndex.toLong() * chunkSize
raf.seek(position)
val buffer = ByteArray(chunkSize)
val bytesRead = raf.read(buffer)
return if (bytesRead == buffer.size) buffer else buffer.copyOf(bytesRead)
}
}
}
7.3 Обработка таймаутов и ретраев
class RetryUploader(
private val maxRetries: Int = 3,
private val initialDelay: Long = 1000,
private val maxDelay: Long = 10000
) {
suspend fun <T> withRetry(
block: suspend () -> Result<T>
): Result<T> {
var currentDelay = initialDelay
var lastError: Throwable? = null
repeat(maxRetries) { attempt ->
val result = block()
if (result.isSuccess) {
return result
}
lastError = result.exceptionOrNull()
// Экспоненциальная backoff с джиттером
val delayWithJitter = (currentDelay * (0.5 + Math.random())).toLong()
delay(minOf(delayWithJitter, maxDelay))
currentDelay *= 2
}
return Result.failure(lastError ?: Exception("Max retries exceeded"))
}
}
7.4 Пакетная загрузка
class BatchUploader {
private val MAX_CONCURRENT_UPLOADS = 2
suspend fun uploadBatch(files: List<File>, authToken: String): Map<File, Result<UploadResponse>> {
return files.chunked(MAX_CONCURRENT_UPLOADS).flatMap { chunk ->
chunk.map { file ->
async { file to uploadSingle(file, authToken) }
}.awaitAll()
}.toMap()
}
private suspend fun uploadSingle(file: File, authToken: String): Result<UploadResponse> {
// Реализация загрузки одного файла
}
}
📋 8. Приложения
8.1 Таблица поддерживаемых форматов
| Тип файла | Расширения | Максимальный размер | Особенности |
|---|---|---|---|
| Основные данные | .cbe, .rta, .bsl, .dat | 100 MB | Требуют валидации сигнатур |
| Изображения | .jpg, .jpeg, .png, .gif, .webp, .bmp | 10 MB | Проверка MIME-типов |
| Текстовые | .txt, .log, .csv | 50 MB | Кодировка UTF-8 |
| Архивы | .zip, .rar, .7z | 200 MB | Требуют проверки на вирусы |
8.2 Коды ошибок и решения
| Код HTTP | Сообщение | Причина | Решение |
|---|---|---|---|
| 400 | No files uploaded | Не переданы файлы | Проверить multipart/form-data |
| 400 | Exactly one main file is required | Не 1 основной файл | Передать ровно 1 не-изображение |
| 400 | Invalid image file format | Поврежденное изображение | Переконвертировать изображение |
| 403 | Invalid or expired authentication token | Просрочен токен | Обновить токен через /auth.php |
| 413 | File exceeds maximum size | Файл слишком большой | Разбить на части или сжать |
| 429 | Daily upload limit exceeded | Достигнут лимит | Ожидать сутки или запросить увеличение |
| 429 | Too many requests | Rate limiting | Увеличить интервалы между запросами |
| 500 | Data too long for column ‘SystemPath’ | Слишком длинный путь | Укоротить имена файлов/папок |
| 500 | Internal server error | Внутренняя ошибка | Связаться с поддержкой |
8.3 Примеры конфигурационных файлов
8.3.1 AndroidManifest.xml (разрешения)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Интернет -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Для Android 6.0+ -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- Для Android 13+ -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!-- Для работы с камерой (если нужно) -->
<uses-permission android:name="android.permission.CAMERA" />
<application>
<!-- Для Android 7.0+ FileProvider -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
8.3.2 res/xml/file_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="cache" path="." />
<external-cache-path name="external_cache" path="." />
<external-files-path name="external_files" path="." />
<files-path name="files" path="." />
</paths>
8.4 Скрипты для тестирования
8.4.1 Bash-скрипт для нагрузочного тестирования
#!/bin/bash
# stress_test_upload.sh
API_URL="https://api.batterywizard.ru/v2/upload.php"
TOKEN="your_token_here"
TEST_FILE="test_file.rta"
# Создание тестового файла
dd if=/dev/urandom of="$TEST_FILE" bs=1M count=10
for i in {1..100}; do
echo "Upload attempt $i"
curl -X POST "$API_URL" \
-H "Authorization: Bearer $TOKEN" \
-F "upload_file=@$TEST_FILE" \
-F "filename=test_$i.rta" \
-F "version=1.0" \
-F "uuid=test-$i" \
-F "os=Linux" \
-F "carrier=Test" \
--max-time 30 \
--silent \
--output /dev/null \
--write-out "HTTP: %{http_code}, Time: %{time_total}s\n"
sleep 1
done
rm "$TEST_FILE"
🎯 Заключение
Эндпоинт /api/v2/upload.php предоставляет мощный и гибкий механизм загрузки файлов с поддержкой сложных бизнес-сценариев. При правильной реализации на клиенте (особенно мобильном) обеспечивается:
- Надежность — транзакционность, retry-логика, валидация
- Безопасность — многоуровневая защита, rate limiting
- Гибкость — поддержка различных форматов, контент-типов
- Отказоустойчивость — обработка сетевых проблем, таймаутов
- Мониторинг — детальное логирование, тестовый режим
Для максимальной эффективности рекомендуется:
- Использовать рекомендуемые библиотеки и паттерны
- Реализовать прогресс-индикацию для больших файлов
- Добавить обработку прерываний и возобновления загрузки
- Регулярно обновлять токены аутентификации
- Мониторить лимиты использования API
Техническая поддержка:
При возникновении проблем предоставляйте request_id из ответа API для быстрой диагностики.
Обновления API:
Следите за изменениями в документации BatteryWizard и заголовке X-API-Version в ответах сервера.
Документация актуальна для версии API 2.20.2 (декабрь 2025)
Последнее обновление: 2025-12-09
