目前網路有關 django 圖片上傳的教學,清一色都是使用 ModelForm。也就是使 ModelForm製作上傳表單,然後寫入伺服器的 SQLite資料庫,再儲存到伺服器的硬碟。而且大家抄來抄去的,實在很傷眼。
本篇並不用ModelForm,而是直接自已製作Form,自已接收圖片,而且不使用SQLite儲存資料。
前端
前端是使用者要上傳圖片的介面,請在 templates目錄下新增 upload.html,設計一個 form 表單,代碼如下。
<!DOCTYPE html> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="/upload" method="post" enctype="multipart/form-data">{% csrf_token %} <input type="text" name="userAccount">
<input type="password" name="userPassword">
<input type="file" name="userFile"> <input type="submit" value="提交"> </form> </body> </html>
Django 預設會啟動跨站請求攻擊保護(Cross-Site Request Forgery),所以在表單的後面,要加入 {% csrf token %} 才可以正確傳送資料。
跨站攻擊這部份日後再說明,可先參考如下某網大所寫的文章 https://openhome.cc/Gossip/CodeData/PythonTutorial/FormCSRF.html。
接下來,於DOS下進入專案目錄,新增 upload app,如入指令
..\venv\Scripts\python.exe manager.py startapp upload
上述指令會在虛擬網站下產生 upload 目錄,再於此目錄下的 views.py 編寫如下代碼
from django.shortcuts import render def html(request): return render(request, 'upload.html',None)
安裝upload app,請修改 settings.py 如下
INSTALLED_APPS = [
'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'upload',
]
修改 urls.py 設定連結網址
import upload.views as upload
urlpatterns = [
path('admin/', admin.site.urls),
path('upload/', upload.html),
]
最後在DOS下啟動虛擬網站,網址輸入 http://localhost:7000/upload 即可看到上傳圖片的網頁
python manager.py runserver 0.0.0.0:7000
後端
上述 upload.html,表單裏的action還是 /upload,表示按下送出圖片時,還是由 upload/ 來處理。這時就需使用 if request.method ==’POST’來判斷是第一次進入網頁,還是第二次已夾帶圖片送出。
from django.http import HttpResponse
from django.shortcuts import render
def html(request):
if request.method == "POST":
userAccount = request.POST.get('userAccount')
userPassword = request.POST.get('userPassword')
if userAccount=='帳號' and userPassword=='密碼':
path = request.POST.get('path')
img = request.FILES.get('userFile')
print(userAccount, userPassword, path, img)
url = 'D:/server/django/media/' + str(img)
with open(url, 'wb') as f:
for data in img.chunks():迴圈每次讀取一部分圖片內容,載入到記憶體中
f.write(data)
return HttpResponse('success')
else:
return HttpResponse('error')
else:
return render(request, 'upload.html', locals())
圖片送出後,upload.py 會將圖片置於上述的 url 路徑中。上述代碼中,不論是圖片還是其它檔案,都可以一併上傳。所以檢查 userAccount及userPassword是否正確,如果正確才允許儲存檔案。
Android 上傳
Android 上傳到 Django 表單時,中文資料會出現亂碼。要解決此問題,需將傳送資料存放在 StringBuffer 中,再用 DataOutputStream 的 write.toByteArray(charset(“UTF-8”)) 傳出,這樣在 Django 就可以接收到正確的中文字。
package com.asuscomm.mahaljsp.aipatrol
import android.content.Context
import android.database.Cursor
import android.location.Geocoder
import android.net.Uri
import android.os.Environment
import android.util.Log
import java.io.DataOutputStream
import java.io.File
import java.io.FileInputStream
import java.net.HttpURLConnection
import java.net.URL
import java.text.SimpleDateFormat
import java.util.*
class ThreadDb(val context: Context):Thread() {
var runFlag=true
override fun run(){
while(runFlag){
val cr=Localdb.getCursor("patrol", null, null)
Log.d("Thomas", "search")
while(cr?.moveToNext()?:false) {
if(uploadImage(cr!!)){
Log.d("Thomas", "上傳成功")
}
else{
Log.d("Thomas", "上傳失敗")
}
}
try {
Thread.sleep(3000)
}
catch(e:InterruptedException){
}
}
}
fun uploadImage(cr: Cursor):Boolean{
try {
val userId = cr?.getString(1)//userId
val carNo = cr?.getString(2)//carNo
val lng = cr?.getDouble(3)//lng
val lat = cr?.getDouble(4)//lat
val eventTime = cr?.getDouble(5)//eventTime
val speed = cr?.getDouble(6)//speed
val addrArray = getAddress(lat, lng)
val photoFile = cr?.getString(8) + ".jpg"//photoName
val lineEnd = "\r\n"
val prefix = "--"
val boundary = UUID.randomUUID().toString()
val maxBufferSize = 1 * 1024 * 1024
val photo = G.dataDir + "/" + photoFile
val file =
File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
photo
)
Log.d("Thomas", file.toString())
val uri = Uri.fromFile(file)
val fd =
context.contentResolver.openFileDescriptor(uri, "r")?.fileDescriptor ?: return false
val fis = FileInputStream(fd)
val path = "20220212"
val url = URL("${G.uploadUrl}/")
val conn = url.openConnection() as HttpURLConnection
conn.readTimeout = 10000
conn.connectTimeout = 10000
conn.doInput = true
conn.doOutput = true
conn.useCaches = false
conn.requestMethod = "POST"
conn.setRequestProperty("Connection", "Keep-Alive")
conn.setRequestProperty("Charset", "utf-8")
conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary)
val dos = DataOutputStream(conn.outputStream)
//傳送 post相關參數
val post: MutableMap<String, String> = mutableMapOf()
post["userAccount"] = G.uploadAccount
post["userPassword"] = G.uploadPassword
post["userId"] = userId
post["carNo"] = carNo
post["lat"] = lat.toString()
post["lng"] = lng.toString()
post["area"] = addrArray[0]
post["address"] = addrArray[1]
post["speed"] = speed.toString()
post["day"] = SimpleDateFormat("yyyy-MM-dd").format(eventTime)
post["time"] = SimpleDateFormat("HH:mm:ss.SSS").format(eventTime)
//由 Android 傳送到 Django 伺服器時,中文會產生亂碼, StringBuffer 的 toByteArray(charset("utf8")) 是正確的解決方案
for (key in post.keys) {
Log.d("Thomas", key + ":" + post[key])
val strBuf=StringBuffer()
strBuf.append(prefix + boundary + lineEnd)
strBuf.append("Content-Disposition: form-data; name=\"%s\"\r\n".format(key))
strBuf.append("Content-Type: text/plain; charset=utf-8\r\n")
strBuf.append("Content-Transfer-Encoding: 8bit\r\n")
strBuf.append("\r\n")
strBuf.append("%s".format(post[key]))
strBuf.append("\r\n")
dos.write(strBuf.toString().toByteArray(charset("UTF-8")))
}
//傳送圖片檔案
dos.writeBytes(prefix + boundary + lineEnd)
dos.writeBytes(
"Content-Disposition: form-data; name=\"photoFile\";filename=\"%s\"\r\n".format(
photoFile
)
)
dos.writeBytes(lineEnd)
var bytesAvailable = fis.available()
var bufferSize = Math.min(bytesAvailable, maxBufferSize)
val buffer = ByteArray(bufferSize)
var bytesRead = fis.read(buffer, 0, bufferSize)
while (bytesRead > 0) {
dos.write(buffer, 0, bufferSize)
bytesAvailable = fis.available()
bufferSize = Math.min(bytesAvailable, maxBufferSize)
bytesRead = fis.read(buffer, 0, bufferSize)
}
dos.writeBytes(lineEnd)
dos.writeBytes(prefix + boundary + prefix + lineEnd)
val webResponse = StringBuffer()
val input = conn.inputStream
var c = 0
while (c != -1) {
c = input.read()
webResponse.append(c.toChar())
}
fis.close()
dos.flush()
dos.close()
if (webResponse.contains("success")) return true
else return false
}
catch(e:Exception){
return false
}
}
private fun getAddress(lat:Double, lng:Double):Array<String>{
val geocoder = Geocoder(context, Locale.TAIWAN)
try {
val addressList = geocoder.getFromLocation(lat, lng, 1)
if (addressList != null && addressList.size > 0) {
val addr = addressList.get(0)
val postalCode = addr.postalCode
val countryName = addr.countryName
val admin = addr.adminArea ?: ""//台北市, 高雄市
val subAdmin = addr.subAdminArea ?: ""//彰化縣
var strArea = addr.locality ?: ""//南港區, 彰化市, 秀水鄉
val addrLine = addr.getAddressLine(0)
val strAddr = addrLine.replace(postalCode, "")
.replace(countryName, "")
.replace(admin, "")
.replace(subAdmin, "")
.replace(strArea, "")
.replace("[\\[\\]+$/\\\\:;*?\"@#%^&'<>|_ ]", "")
.replace("\n", "")
.replace("\r", "")
strArea=strArea.replace("区", "區")
return arrayOf(strArea,strAddr)
} else return arrayOf("例外區","Google圖資無此地資料")
}
catch(e:Exception){
return arrayOf("例外區","Google圖資無此地資料")
}
}
fun close(){
runFlag=false
}
}
todo