import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_advanced_networkimage/provider.dart'; import 'package:image/image.dart'; import 'package:path/path.dart' as path; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; /** * InvenTree API - Access to the InvenTree REST interface. * * InvenTree implements token-based authentication, which is * initialised using a username:password combination. */ class InvenTreeAPI { // Endpoint for requesting an API token static const _URL_GET_TOKEN = "user/token/"; static const _URL_GET_VERSION = ""; // Base URL for InvenTree API e.g. http://192.168.120.10:8000 String _BASE_URL = ""; // Accessors for various url endpoints String get baseUrl { String url = _BASE_URL; if (!url.endsWith("/")) { url += "/"; } return url; } String _makeUrl(String url) { if (url.startsWith('/')) { url = url.substring(1, url.length); } url = url.replaceAll('//', '/'); return baseUrl + url; } String get apiUrl { return _makeUrl("/api/"); } String get imageUrl { return _makeUrl("/image/"); } String makeApiUrl(String endpoint) { return _makeUrl("/api/" + endpoint); } String makeUrl(String endpoint) { return _makeUrl(endpoint); } String _username = ""; String _password = ""; // Authentication token (initially empty, must be requested) String _token = ""; bool isConnected() { return _token.isNotEmpty; } /* * Check server connection and display messages if not connected. * Useful as a precursor check before performing operations. */ bool checkConnection(BuildContext context) { // Firstly, is the server connected? if (!isConnected()) { showDialog( context: context, child: new SimpleDialog( title: new Text("Not Connected"), children: [ ListTile( title: Text("Server not connected"), ) ] ) ); return false; } // Is the server version too old? // TODO // Finally return true; } // Server instance information String instance = ''; // Server version information String _version = ''; // Getter for server version information String get version => _version; // Connection status flag - set once connection has been validated bool _connected = false; bool get connected { return _connected && baseUrl.isNotEmpty && _token.isNotEmpty; } // Ensure we only ever create a single instance of the API class static final InvenTreeAPI _api = new InvenTreeAPI._internal(); factory InvenTreeAPI() { return _api; } InvenTreeAPI._internal(); Future connect() async { var prefs = await SharedPreferences.getInstance(); String server = prefs.getString("server"); String username = prefs.getString("username"); String password = prefs.getString("password"); return connectToServer(server, username, password); } Future connectToServer(String address, String username, String password) async { /* Address is the base address for the InvenTree server, * e.g. http://127.0.0.1:8000 */ String errorMessage = ""; address = address.trim(); username = username.trim(); if (address.isEmpty || username.isEmpty || password.isEmpty) { errorMessage = "Server Error: Empty details supplied"; print(errorMessage); throw errorMessage; } if (!address.endsWith('/')) { address = address + '/'; } // TODO - Better URL validation /* * - If not a valid URL, return error * - If no port supplied, append a default port */ _BASE_URL = address; _username = username; _password = password; _connected = false; print("Connecting to " + apiUrl + " -> " + username + ":" + password); // TODO - Add connection timeout var response = await get("").timeout(Duration(seconds: 10)).catchError((error) { if (error is SocketException) { print("Could not connect to server"); return null; } else if (error is TimeoutException) { print("Server timeout"); return null; } else { // Unknown error type, re-throw error print("Unknown error: ${error.toString()}"); throw error; } }); if (response == null) { return false; } if (response.statusCode != 200) { print("Invalid status code: " + response.statusCode.toString()); return false; } var data = json.decode(response.body); print("Response from server: $data"); // We expect certain response from the server if (!data.containsKey("server") || !data.containsKey("version")) { errorMessage = "Server resonse contained incorrect data"; print(errorMessage); throw errorMessage; } print("Server: " + data["server"]); print("Version: " + data["version"]); _version = data["version"]; // Record the instance name of the server instance = data['instance'] ?? ''; // Request token from the server if we do not already have one if (_token.isNotEmpty) { print("Already have token - $_token"); return true; } // Clear out the token _token = ""; response = await get(_URL_GET_TOKEN).timeout(Duration(seconds: 10)).catchError((error) { print("Error requesting token:"); print(error); return false; }); if (response.statusCode != 200) { print("Invalid status code: " + response.statusCode.toString()); return false; } else { var data = json.decode(response.body); if (!data.containsKey("token")) { print("No token provided in response"); return false; } // Return the received token _token = data["token"]; print("Received token - $_token"); _connected = true; return true; }; } // Perform a PATCH request Future patch(String url, {Map body}) async { var _url = makeApiUrl(url); var _headers = defaultHeaders(); var _body = Map(); // Copy across provided data body.forEach((K, V) => _body[K] = V); print("PATCH: " + _url); return http.patch(_url, headers: _headers, body: _body, ); } // Perform a POST request Future post(String url, {Map body}) async { var _url = makeApiUrl(url); var _headers = jsonHeaders(); print("POST: ${_url} -> ${body.toString()}"); var data = jsonEncode(body); return http.post(_url, headers: _headers, body: data, ); } // Perform a GET request Future get(String url, {Map params}) async { var _url = makeApiUrl(url); var _headers = defaultHeaders(); // If query parameters are supplied, form a query string if (params != null && params.isNotEmpty) { String query = '?'; params.forEach((K, V) => query += K + '=' + V + '&'); _url += query; } // Remove extraneous character if present if (_url.endsWith('&')) { _url = _url.substring(0, _url.length - 1); } print("GET: " + _url); return http.get(_url, headers: _headers); } Map defaultHeaders() { var headers = Map(); headers[HttpHeaders.authorizationHeader] = _authorizationHeader(); //headers['Authorization'] = _authorizationHeader(); return headers; } Map jsonHeaders() { var headers = defaultHeaders(); headers['Content-Type'] = 'application/json'; return headers; } String _authorizationHeader () { if (_token.isNotEmpty) { return "Token $_token"; } else { return "Basic " + base64Encode(utf8.encode('$_username:$_password')); } } static String get staticImage => "/static/img/blank_image.png"; static String get staticThumb => "/static/img/blank_image.thumbnail.png"; /* * Get an image from the server (or, from cache) */ AdvancedNetworkImage getImage(String imageUrl) { if (imageUrl.isEmpty) { imageUrl = staticImage; } String url = makeUrl(imageUrl); return new AdvancedNetworkImage(url, header: defaultHeaders(), useDiskCache: true, //retryDuration: const Duration(seconds: 2), //retryLimit: 3, cacheRule: CacheRule(maxAge: const Duration(days: 1)), loadFailedCallback: () { DiskCache().evict(url); } ); } }