0
1

クラウドストレージサービスを自作した話

Last updated at Posted at 2024-06-03

はじめに

結構前に作りました。当時は金がなく、google driveを契約できるほどの経済力が全くなかったのですが、その時思いました。無いなら自分で作ってしまえばいいと。

ハードウェア

都合のいいことに余っていたpcパーツがありました。流石に全て揃っていたわけではないのでCPUとマザーボードは買い足しました。これらのパーツでサーバーとなるPCを新たに組み、ハードウェアを用意することができました。4TBのHDDが転がっていたのも大変都合がよかった。

構想

Djangoを使ってシステムを構築し、bootstrapでいい感じに整えようと考えました。
アカウント毎のデータの管理、データ残量、アップロード制限、パスワードの変更、アカウントの新規作成、pdfファイルのプレビュー、動画ファイルのサムネイル作成自動化、サムネイル画像のデータサイズの自動縮小、スクロールによるページング、レスポンシブ対応、など諸々の機能を実装する。

完成後のホーム画面

無題402_20240603141831.JPG

Djangoの各ファイル設定

無題401_20240531202439.jpg
無題401_20240531202517.jpg
スクリーンショット 2024-05-31 20.12.50.png
無題401_20240531202535.jpg

Python系のファイル

SETTINGS

settings.pyはプロジェクト全体の設定を記述する場所。デバッグモードのオンとオフ、アプリの追加、staticやmediaの場所の定義、データベースの指定など、ありとあらゆる設定はこのファイルに記述する。

settings.py

"""
Django settings for cloud_storage project.

Generated by 'django-admin startproject' using Django 4.1.4.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/
"""

from pathlib import Path
import os

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-ni=y&kyt)n%k$mr32j=s^yc1lj_*#w86_*ob(5&jpyxit604%("

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False

ALLOWED_HOSTS = ['*']
CSRF_TRUSTED_ORIGINS = ['http://example']


# Application definition

INSTALLED_APPS = [
    'main',
    'widget_tweaks',
    'django_cleanup',
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
'whitenoise.middleware.WhiteNoiseMiddleware',
]

ROOT_URLCONF = "cloud_storage.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [os.path.join(BASE_DIR, 'templates')],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

WSGI_APPLICATION = "cloud_storage.wsgi.application"


# Database
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}


# Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
    },
    {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",},
    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",},
    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",},
]


# Internationalization
# https://docs.djangoproject.com/en/4.1/topics/i18n/

LANGUAGE_CODE = "ja"

TIME_ZONE = "Asia/Tokyo"

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/

STATIC_URL = "static/"

STATIC_ROOT = r"example"
# Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field

LOGIN_URL = "/TCloud/login/"

LOGIN_REDIRECT_URL = "/TCloud/"
LOGOUT_REDIRECT_URL = "/TCloud/login/"



DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"


MEDIA_URL = '/media/'
if DEBUG:
    MEDIA_ROOT = r"example" #メディアディクレトリのパスを指定
else:
    MEDIA_ROOT = r"example"


ASGI

asgi.py

"""
ASGI config for cloud_storage project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cloud_storage.settings")

application = get_asgi_application()


URLS

プロジェクト全体のurls.pyで、URLで特定の文字列を指定された際に、登録されているアプリケーションに誘導する記述がある。

cloud_strage/urls.py

"""cloud_storage URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/4.1/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from django.contrib.auth.decorators import login_required
from main import views

from django.conf.urls.static import static
from django.views.static import serve  #追加
from django.urls import re_path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("TCloud/", include("main.urls")),
    path("TCloud/", include("django.contrib.auth.urls")),
    re_path(r'^media/(?P<path>.*)$', serve, kwargs={'document_root': settings.MEDIA_ROOT})
]

#if settings.DEBUG:
    #urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)


WSGI

wsgi.py

"""
WSGI config for cloud_storage project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cloud_storage.settings")

application = get_wsgi_application()


URLS

このurls.pyはアプリの中で定義される。投稿や削除、検索機能やログイン、新規ユーザー登録機能を実装するのに必須なもの。

main/urls.py

from django.urls import path

from . import views
from django.conf import settings
from django.conf.urls.static import static



urlpatterns = [
    path('', views.Index.as_view(), name="index"),
    path('product/<pk>/', views.Detail.as_view(), name="detail"),
    path('create/', views.Create.as_view(), name="create"),
    path('delete/<pk>', views.Delete.as_view(), name="delete"),
    path('search', views.SearchView.as_view(), name="search"),
    path("signup/", views.SignUpView.as_view(), name="signup"),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)


VIEWS

views.pyはアプリの中でのみ定義される。アプリの主な機能について記述する。投稿や削除、編集など、諸々の機能の挙動は、このviews.pyに記述する。

views.py

from django.shortcuts import render, redirect
from django.views.generic import ListView, DetailView, TemplateView
from .models import Post
from django.views.generic.edit import CreateView
from django.views.generic.edit import UpdateView
from django.views.generic.edit import DeleteView
from django.views.generic import View
from .forms import UploadForm, SignUpForm
from django.urls import reverse, reverse_lazy
from django.http import HttpResponseRedirect
import os
import sys
import subprocess
import codecs
from django.contrib.auth.decorators import login_required # defで記載するものの規制用
from django.contrib.auth.mixins import LoginRequiredMixin # classで記載するものの規制用
from django.db.models import Q
from functools import reduce
from operator import and_
from django.contrib import messages
from django.contrib.auth.models import User
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.views import generic
import time
import shutil
from django.http import FileResponse
from PIL import Image
import cv2


media_dir = r"F:\DATE\server_media\cloud_media/"

# Create your views here.
class Index(LoginRequiredMixin,generic.ListView):

    # 一覧するモデルを指定 -> `object_list`で取得可能
    model = Post

    context_object_name = 'post_list'
    queryset = Post.objects.order_by('-file_name')

    def get_queryset(self):
        queryset = Post.objects.order_by('-id')
        query = self.request.GET.get('query')
        if query:
            queryset = queryset.filter(
            Q(file_name__icontains=query)
            )

        #c_num = queryset.count()#検索結果の値が入ってるよ
        #params = {'c_num' : str(c_num)}

        return queryset

# DetailViewは詳細を簡単に作るためのView
class Detail(LoginRequiredMixin,DetailView):
    # 詳細表示するモデルを指定 -> `object`で取得可能
    model = Post



class Create(LoginRequiredMixin,CreateView):
    template_name = "main/post_form.html"
    model = Post
    form_class = UploadForm
    #success_url = reverse_lazy('/')
    def post(self, request):
        if request.method == 'POST':
            form = UploadForm(request.POST, request.FILES)
            if form.is_valid():
                for ff in request.FILES.getlist('file_name'):
                    re_user_name = request.user.username
                    p = Post(file_name=ff,user_name=re_user_name)
                    p.save()

                    upload_file_name = ff.name
                    upload_file_name = upload_file_name.replace(" ","_")
                    upload_file_name = upload_file_name.replace("(","")
                    upload_file_name = upload_file_name.replace(")","")

                    account_dir = str(media_dir) + str(re_user_name) + '/'
                    if os.path.isdir(account_dir) == False :
                        os.mkdir(str(media_dir) + str(re_user_name))

                    root, ext = os.path.splitext(str(upload_file_name))
                    ext = ext.replace('.','')

                    if str(ext) == 'jpg' or str(ext) == 'jpeg' or str(ext) == 'png' or str(ext) == 'webp' :
                        try:
                            bairitsu = 8 #倍率指定
                            imagedata = Image.open(str(media_dir) + str(upload_file_name))
                            width, height = imagedata.size
                            width2 = width/bairitsu
                            height2 = height/bairitsu
                            imagedata2= imagedata.resize((int(width2),int(height2)))
                            newimage = str(media_dir) + 'low_' + str(upload_file_name)
                            imagedata2.save(newimage, quality=85,optimize=True)
                            del imagedata
                            del imagedata2
                        except Exception as e:
                            print(e)



                    try:
                        shutil.copy(str(media_dir) + str(upload_file_name), str(account_dir) + str(upload_file_name))
                        os.remove(str(media_dir) + str(upload_file_name))
                        if str(ext) == 'jpg' or str(ext) == 'jpeg' or str(ext) == 'png' or str(ext) == 'webp' :
                            shutil.copy(str(media_dir) + 'low_' + str(upload_file_name), str(account_dir) + 'low_' + str(upload_file_name))
                            os.remove(str(media_dir) + 'low_' + str(upload_file_name))

                    except Exception as e:
                        print(e)

            return HttpResponseRedirect('/TCloud/')

        return render(request, 'TCloud/post_form.html', {'form': form})

    success_url = '/TCloud/'


class Delete(LoginRequiredMixin,DeleteView):
    model = Post
    # 削除したあとに移動する先(トップページ)
    success_url = "/TCloud/"

    def post(self, request,**kwargs):
        try:
            date = Post.objects.get(id=int(kwargs['pk']))
            file_name_re = date.file_name
            re_user_name = request.user.username
            date.delete()
            os.remove(str(media_dir) + str(re_user_name) + '/' + str(file_name_re))

            root, ext = os.path.splitext(str(file_name_re))
            ext = ext.replace('.','')

            if str(ext) == 'jpg' or str(ext) == 'jpeg' or str(ext) == 'png' or str(ext) == 'webp' :
                os.remove(str(media_dir) + str(re_user_name) + '/' + 'low_' + str(file_name_re))

        except :
            pass
        return HttpResponseRedirect('/TCloud/')

class SignUpView(CreateView):
    template_name = 'registration/signup.html'
    form_class = SignUpForm
    success_url = reverse_lazy('login')

class SearchView(View):
    def get(self, request, *args, **kwargs):
        post_data = Post.objects.order_by('-id')
        keyword = request.GET.get('keyword')

        if keyword:
            post = post_data.filter(
                     Q(title__icontains=keyword)
                   )
            #messages.success(request, '「{}」の検索結果'.format(keyword))
        return render(request, 'main/post_list.html', {'post': post })


MODELS

models.pyはデータベースの設計を行うことができる。

models.py

from django.db import models
from django.urls import reverse_lazy
from django.core.validators import FileExtensionValidator
from django.conf import settings
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.dispatch import receiver
from django.utils import timezone

class Post(models.Model):
    created = models.DateTimeField(
        auto_now_add=True,
        editable=False,
        blank=False,
        null=False)

    user_name = models.CharField(default=256,max_length=256,blank=False,null=False)

    file_name = models.FileField(blank=False, null=False, verbose_name='ファイル')

    def get_absolute_url(self):
        return reverse_lazy("detail", args=[self.id])


FORMS

forms.pyではhtmlのテンプレートで用いるフォームを定義しておける。ここで定義しておくことによって、テンプレートでの記述を少なく済ませることができる。

forms.py

from .models import Post
from django import forms
from django.core.exceptions import ValidationError
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import get_user_model
from django.urls import reverse_lazy
from django.conf import settings


class UploadForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ('file_name','user_name',)


User = get_user_model()

class SignUpForm(UserCreationForm):
    class Meta:
        model = User
        fields = ("username", "password1", "password2")


TAG_LIBRARY

tag_library.pyでは、htmlのテンプレートから呼び出せる機能を独自に定義することができる。もともとDjangoにはすでに定義されている機能があるが、これを用いればオリジナルの機能を作り出すことができる。

  • elif func == str(1):elif func == str(2):elif func == str(3):で、プログレスバーを実装し、使用可能データ容量の残量表示を実装
  • elif func == str(4):elif func == str(5):で残量を参照し、アップロード可能の可否を判定することで容量制限を実装
templatetags/tag_library.py

from django import template
import os
import math
register = template.Library()
#@register.filter(name="ext")
@register.filter()

def ext(value,func):
    hund_twen = 128849018880
    media_dir = r"F:\DATE\server_media\cloud_media/"
    try :
        if func == str(1):
            path = str(media_dir) + str(value)
            total = 0
            with os.scandir(path) as it:
                for entry in it:
                    if entry.is_file():
                        total += entry.stat().st_size
                    elif entry.is_dir():
                        total += get_dir_size(entry.path)
            export = '{:.2%}'.format(int(total) / int(hund_twen)) #120GB,128849018880
        elif func == str(2):
            path = str(media_dir) + str(value)
            total = 0
            with os.scandir(path) as it:
                for entry in it:
                    if entry.is_file():
                        total += entry.stat().st_size
                    elif entry.is_dir():
                        total += get_dir_size(entry.path)
            units = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB")
            i = math.floor(math.log(total, 1024)) if total > 0 else 0
            total = round(total / 1024 ** i, 2)
            export = f"{total} {units[i]}"
        elif func == str(3):
            path = str(media_dir) + str(value)
            total = 0
            with os.scandir(path) as it:
                for entry in it:
                    if entry.is_file():
                        total += entry.stat().st_size
                    elif entry.is_dir():
                        total += get_dir_size(entry.path)
            total = int(hund_twen) - total
            if total >= 10737418240 :
                export = 'bg-info'
            else :
                export = 'bg-danger'
        elif func == str(4):
            try :
                path = str(media_dir) + str(value)
                total = 0
                with os.scandir(path) as it:
                    for entry in it:
                        if entry.is_file():
                            total += entry.stat().st_size
                        elif entry.is_dir():
                            total += get_dir_size(entry.path)

                if total >= int(hund_twen) :
                    export = 'disabled'
                else:
                    export = 'active'
            except :
                export = 'active'
        elif func == str(5):
            path = str(media_dir) + str(value)
            total = 0
            with os.scandir(path) as it:
                for entry in it:
                    if entry.is_file():
                        total += entry.stat().st_size
                    elif entry.is_dir():
                        total += get_dir_size(entry.path)

            if total >= int(hund_twen) :
                export = ''
            else:
                export = 'hidden'
        else:
            path = str(media_dir) + str(value) + '/' + str(func)
            total = os.path.getsize(path)
            units = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB")
            i = math.floor(math.log(total, 1024)) if total > 0 else 0
            total = round(total / 1024 ** i, 2)
            export = f"{total} {units[i]}"
    except:
        export = 'hidden'
    return str(export)


INIT

init.pyはtag_libraryのあるフォルダに必ず作成しておかなくてはならないファイル。
中身は空で良い。

__init__.py

テンプレートなどのHTML、CSS、等

javascriptや主に使っているCSSはbootstrapなので主にhtmlの記述になる。

_form.html

これは、投稿や編集などのフォームを定義するテンプレート。

tempates/_form.html

{% load static %}
{% load widget_tweaks %}
<form method="post">
    {% csrf_token %}
    {% for field in form %}
    <div>
      <p class="texth">
        <label>{{ field.label }}</label>
      </p>
        {% render_field field class='text11' %}
    </div>

    {% endfor %}
    <input type="submit" value="{{ submit_label }}" class="btn btn-primary"/>
</form>


post_list.html

これはデータベースに登録されている要素をリストで取得し一覧表示することができる。これがメインの画面。

tempates/main/post_list.html

{% extends "main/base.html" %}
{% load static %}

{% load tag_library %}

{% block main %}

{% if user.username == 'tyakyumyou_cloud' %}
<h6 class="mt-2 text-center h6">{{ user.username|ext:'2' }}使用中</h6>
{% else %}
<div>
  <div class="progress mt-3 mx-4" style="height: 23px;" role="progressbar" aria-valuenow="{{ user.username|ext:'1' }}" aria-valuemin="0" aria-valuemax="100">
    <div class="progress-bar {{ user.username|ext:'3' }}" style="width:{{ user.username|ext:'1' }}"></div>
  </div>
  {% if user.username|ext:'2' != 'hidden' %}
  <p class="text-center">{{ user.username|ext:'1' }}</p>
  <h6 class="text-center">{{ user.username|ext:'2' }}使用中</h6>
  {% endif %}
</div>
<div class="alert alert-danger mx-4" role="alert" {{ user.username|ext:'5' }}>アップロード可能な容量がない為ファイルを新規にアップロードできません。ファイルを削除して容量を確保してください。</div>
{% endif %}


<div class="row my-4 mx-2">

  {% if post_list %}

  {% for post in post_list  %}
    {% if post.user_name == user.username %}
    <div class="col-6 col-sm-3 col-md-3 col-lg-2" style="padding: 0;">
        <a class="text-decoration-none link-dark" href="{% url 'detail' post.id %}">
        <div class="card">
            <div class="card-body " style="padding: 0;">
              <div style="height:185px;">
                <script type="text/javascript">
                  var file = '{{ post.file_name }}';
                  var file_name = file.split('.').pop();

                  if (file_name == 'png' || file_name == 'jpg' || file_name == 'gif' || file_name == 'webp' || file_name == 'jpeg') {
                      var file_type_re = '<img class="mw-100 rounded" style="width:100%; height:100%; object-fit: cover;" src="/media/{{ user.username }}/low_{{ post.file_name }}">'
                  document.write(file_type_re);}
                  else if(file_name == 'mp4' || file_name == 'mov' || file_name == 'MP4' || file_name == 'MOV'){
                      var file_type_re = '<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" fill="currentColor" poster="/media/{{ user.username }}/thum_{{ post.file_name }}" class="bi bi-play-btn" viewBox="0 0 16 16"><path d="M6.79 5.093A.5.5 0 0 0 6 5.5v5a.5.5 0 0 0 .79.407l3.5-2.5a.5.5 0 0 0 0-.814l-3.5-2.5z"/><path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4zm15 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4z"/></svg>'
                  document.write(file_type_re);}
                  else if(file_name == 'mp3' || file_name == 'wav' || file_name == 'aac' || file_name == 'webm'){
                      var file_type_re = '<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" fill="currentColor" class="bi bi-music-note-list" viewBox="0 0 16 16"><path d="M12 13c0 1.105-1.12 2-2.5 2S7 14.105 7 13s1.12-2 2.5-2 2.5.895 2.5 2z"/><path fill-rule="evenodd" d="M12 3v10h-1V3h1z"/><path d="M11 2.82a1 1 0 0 1 .804-.98l3-.6A1 1 0 0 1 16 2.22V4l-5 1V2.82z"/><path fill-rule="evenodd" d="M0 11.5a.5.5 0 0 1 .5-.5H4a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5zm0-4A.5.5 0 0 1 .5 7H8a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5zm0-4A.5.5 0 0 1 .5 3H8a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5z"/></svg>'
                  document.write(file_type_re);}
                  else {
                      var file_type_re = '<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" fill="currentColor" class="bi bi-folder2" viewBox="0 0 16 16"><path d="M1 3.5A1.5 1.5 0 0 1 2.5 2h2.764c.958 0 1.76.56 2.311 1.184C7.985 3.648 8.48 4 9 4h4.5A1.5 1.5 0 0 1 15 5.5v7a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5v-9zM2.5 3a.5.5 0 0 0-.5.5V6h12v-.5a.5.5 0 0 0-.5-.5H9c-.964 0-1.71-.629-2.174-1.154C6.374 3.334 5.82 3 5.264 3H2.5zM14 7H2v5.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V7z"/></svg>'
                  document.write(file_type_re);}
                </script>
              </div>
            </div>
          <h5 class="card-header text-truncate">{{ post.file_name }}</h5></a>
          </div>
    </div>
    {% endif %}
  {% endfor %}
  {% else %}
  <h3 class="text-center">ファイルが存在しません</h3>
  {% endif %}

</div>



{% endblock %}


post_form.html

投稿する際に表示されるフォーム

tempates/main/post_form.html

{% extends "main/base.html" %}
{% load static %}
{% load widget_tweaks %}

{% block main %}

<br>
<div class="col-12 col-sm-12 col-md-12 col-lg-12 mx-auto" style='max-width:600px; width:100%; padding: 0;'>
  <form method="POST" enctype="multipart/form-data">
    {% csrf_token %}
  <div class="card">
      <div class="card-body " style="padding: 0;">
        <div style="height:100px;">
          <input class="my-4 form-control form-control-lg" multiple id="formFileLg" type="file" name="file_name" value="" required>
          <input class="not_show" id="formFileLg" type="text" name="user_name" value="{{ user.username }}" required>
        </div>
      </div>
      <input class='mx-4 btn btn-success' type="submit" name="btn_upload" value="アップロード">
    </div>
    </form>
</div>

{% endblock %}


post_detail.py

投稿した内容を編集するに表示されるもの

無題402_20240603141853.JPG

tempates/main/post_deteil.html

{% extends "main/base.html" %}
{% load static %}

{% load tag_library %}

{% block main %}

<div class="row my-2 mx-2">

  <div class="col-12 col-sm-12 col-md-12 col-lg-12 mx-auto" style='max-width:600px; width:100%; padding: 0;'>
      <div class="card mx-auto">
        <h3 class="card-header">{{ object.file_name }}</h3>
          <div class="card-body " style="padding: 0;">
            <div style="width:100%; height:50%;">

              <script type="text/javascript">
                var file = '{{ post.file_name }}';
                var file_name = file.split('.').pop();

                if (file_name == 'png' || file_name == 'jpg' || file_name == 'gif' || file_name == 'webp' || file_name == 'jpeg') {
                    var file_type_re = '<img class="mw-100 rounded" style="width:100%; height:100%; object-fit: cover;" src="/media/{{ user.username }}/{{ post.file_name }}">'
                document.write(file_type_re);}
                else if(file_name == 'mp4' || file_name == 'mov' || file_name == 'MP4' || file_name == 'MOV'){
                    var file_type_re = '<video id="video" controls poster="/media/{{ user.username }}/thum_{{ object.file_name }}" src="/media/{{ user.username }}/{{ object.file_name }}" width="100%" height="100%"></video>'
                document.write(file_type_re);}
                else if(file_name == 'mp3' || file_name == 'wav' || file_name == 'aac' || file_name == 'webm'){
                    var file_type_re = '<audio controls><source src="/media/{{ user.username }}/{{ object.file_name }}"></audio>'
                document.write(file_type_re);}
                else if(file_name == 'pdf'){
                    var file_type_re = '<object data="/media/{{ user.username }}/{{ object.file_name }}" type="application/pdf" width="100%" height="100%"><iframe src="/media/{{ user.username }}/{{ object.file_name }}" width="100%" height="100%"><p><b>表示されない時の表示</b>: <a href="pdf.pdf">PDF をダウンロード</a>.</p></iframe></object>'
                document.write(file_type_re);}
                else if(file_name == 'txt' || file_name == 'py'){
                    var file_type_re = '<object src="/media/{{ user.username }}/{{ object.file_name }}" date="/media/{{ user.username }}/{{ object.file_name }}" type="text/plain"></object>'
                document.write(file_type_re);}
                else {
                    var file_type_re = '<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" fill="currentColor" class="bi bi-folder2" viewBox="0 0 16 16"><path d="M1 3.5A1.5 1.5 0 0 1 2.5 2h2.764c.958 0 1.76.56 2.311 1.184C7.985 3.648 8.48 4 9 4h4.5A1.5 1.5 0 0 1 15 5.5v7a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5v-9zM2.5 3a.5.5 0 0 0-.5.5V6h12v-.5a.5.5 0 0 0-.5-.5H9c-.964 0-1.71-.629-2.174-1.154C6.374 3.334 5.82 3 5.264 3H2.5zM14 7H2v5.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V7z"/></svg>'
                document.write(file_type_re);}
              </script>
            </div>
          </div>
          <a class="my-2 mx-5 btn btn-outline-success" href="/media/{{ user.username }}/{{ object.file_name }}" download="/media/{{ user.username }}/{{ object.file_name }}">ダウンロード</a>
          <a class="my-1 mb-2 mx-5 btn btn-outline-danger" href="{% url 'delete' object.pk %}">削除</a>
        </div>
  </div>
<p class="sp fs-6 mt-2">投稿日時 {{ post.created }}</p>
<p class="sp fs-6">ファイルのサイズ {{ user.username|ext:object.file_name }}</p>

<p class="pc fs-5 mt-2">投稿日時 {{ post.created }}</p>
<p class="pc fs-5">ファイルのサイズ {{ user.username|ext:object.file_name }}</p>



</div>


{% endblock %}


post_confirm_delete.html

投稿したものを削除し、削除が成功した場合に表示されるもの

tempates/main/post_confirm_delete.html

{% extends "main/base.html" %}
{% load static %}

{% block main %}
<div class="row my-2 mx-2">
  <div class="col-12 col-sm-12 col-md-12 col-lg-12 mx-auto" style='max-width:600px; width:100%; padding: 0;'>
    <div class="card mx-auto">
      <h3 class="card-header">{{ object.file_name }}</h3>
        <div class="card-body " style="padding: 0;">
          <div style="width:100%; height:50%;">
            <h2 class="text-center">削除確認</h2>
            <p class="text-center">{{ object.file_name }}を本当に削除してもよろしいですか?</p>
          </div>
        </div>
        <form class="mx-auto" method="post">
            {% csrf_token %}
            <input type="submit" value="削除" class="mx-2 my-2 btn btn-danger" />
        </form>
      </div>
  </div>
</div>

{% endblock %}


base.html

すべてのhtmlテンプレートの基礎的な部分の記述。
通常、他のテンプレートは最初に、このbase.htmlを呼び出し、組み合わせることで使用する。

tempates/main/base.html

<!doctype html>
{% load static %}
<html lang="ja">
  <head>
    <link rel="icon" type="image/png" href="{% static 'favicon.png' %}">
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="{% static 'main.css' %}">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>はらDRIVE</title>
  </head>
  <body>

    {% include "main/_navbar.html" %}

    {% block main %}
    コンテンツがありません。
    {% endblock %}

    <script>
    window.addEventListener('pageshow',()=>{
  if(window.performance.navigation.type==2) location.reload();
  });
    </script>

  <script>
  window.onpageshow = function(event) {
  if (event.persisted) {
   window.location.reload();
  }
  };
  </script>

  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
  </body>
</html>


_navbar.html

ナビゲーションバーを表示するのに必要な記述

tempates/main/_navbar.html

{% load static %}
{% load tag_library %}
<nav class="navbar navbar-expand-sm navbar-dark bg-dark">
   <div class="container-fluid">
     <a class="navbar-brand" href="/TCloud">はらDRIVE</a>
     <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="ナビゲーションの切替">
       <span class="navbar-toggler-icon"></span>
     </button>
     <div class="collapse navbar-collapse" id="navbar">
       <ul class="navbar-nav me-auto mb-2 mb-sm-0">


         {% if user.username == 'tyakyumyou_cloud' %}
         <li class="nav-item">
           <a class="nav-link active" aria-current="page" href="{% url 'create' %}">アップロード</a>
         </li>
         <li class="nav-item dropdown">
           <a href="#" class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">アカウント詳細</a>
           <ul class="dropdown-menu">
             <li><p class='dropdown-item'>{{ user.username }}</p></li>
             <li><a class="dropdown-item" href="{% url 'password_change' %}">パスワードの変更</a></li>
             <li><a class="dropdown-item" href="{% url 'logout' %}">ログアウト</a></li>
           </ul>
         </li>

       </ul>
       <form class="d-flex" role="search" method="get">
         <input type="search" value="{{ request.GET.query }}" class='center input_css form-control' name="query" placeholder="ドライブで検索">
       </form>
         {% elif request.user.is_authenticated %}
         <li class="nav-item">
           <a class="nav-link {{ user.username|ext:'4' }}" aria-current="page" href="{% url 'create' %}">アップロード</a>
         </li>


         <!--<li class="nav-item">
           <a class="nav-link" href="#">リンク</a>
         </li>-->
         <!--<li class="nav-item">
           <a class="nav-link disabled">無効</a>
         </li>-->
         <li class="nav-item dropdown">
           <a href="#" class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">アカウント詳細</a>
           <ul class="dropdown-menu">
             <li><p class='dropdown-item'>{{ user.username }}</p></li>
             <li><a class="dropdown-item" href="{% url 'password_change' %}">パスワードの変更</a></li>
             <li><a class="dropdown-item" href="{% url 'logout' %}">ログアウト</a></li>
           </ul>
         </li>

       </ul>
       <form class="d-flex" role="search" method="get">
         <input type="search" value="{{ request.GET.query }}" class='center input_css form-control' name="query" placeholder="ドライブで検索">
       </form>
       {% else %}

       {% endif %}
     </div>
   </div>
 </nav>


login.html

ログイン画面で表示される

無題402_20240603141717.JPG

tempates/registration/login.html

{% extends "main/base.html" %}

{% block main %}
<form class="form-signin" method="POST" action="">
  {% csrf_token %}
  <h1 class="h3 mb-3 font-weight-normal">ログイン</h1>
  {% if error %}
    <div class="alert alert-danger" role="alert">
      {{ error }}
    </div>
  {% endif %}
  <label for="inputName" class="sr-only">ユーザー名</label>
  <input type="text" id="inputEmail" class="form-control" name="username" placeholder="UserName" required autofocus>
  <label for="inputPassword" class="sr-only">パスワード</label>
  <input type="password" id="inputPassword" class="form-control"  name="password" placeholder="Password" required>
  <button class="btn btn-lg btn-primary btn-block" type="submit">ログイン</button>
  <a href="{% url 'signup' %}"><div class="mx-4 btn btn-success">アカウント作成</div></a>
</form>



{% endblock %}


password_change_done.html

パスワードの変更が完了した際に表示される

tempates/registration/password_change_done.html

{% extends "main/base.html" %}

{% block main %}
<div class="center">
  <br>
  <br>
  <div class="texth2">
    パスワード変更完了
  </div>
  <br>
  <br>

</div>

{% endblock %}


password_change_form.html

アカウントのパスワードを変更する際に表示されるフォーム

tempates/registration/password_change_form.html

{% extends "main/base.html" %}

{% block main %}
<form action="" method="POST">
    {{ form.non_field_errors }}
    {% for field in form %}
    <div class="form-group">
        <label for="{{ field.id_for_label }}">{{ field.label_tag }}</label>
        {{ field }}
        {{ field.errors }}
    </div>
    {% endfor %}
    {% csrf_token %}
    <button type="submit" class="btn btn-primary btn-lg">送信</button>
</form>
{% endblock %}


signup.html

アカウント作成の際に表示される。

無題402_20240603141745.JPG

tempates/registration/signup.html

{% extends "main/base.html" %}

{% block main %}
<form class="form-signin" method="POST">
  {% csrf_token %}
  <div class="alert alert-secondary" role="alert">
    <h5 class="text-center">注意</h5>
    <p class="text-center">ユーザー名は英数字のみ使用可能。<br>
  「(」「)」「-」スペースは使えない。<br>記号の使用はなるべく控えてほしい。<br>
  代わりに「_」アンダーバーを推奨。<br>
  <br>
  パスワードは英数字大文字小文字で<br>8文字以上</p>
  </div>
  <h1 class="h3 mb-3 font-weight-normal">アカウント作成</h1>
  {% if error %}
    <div class="alert alert-danger" role="alert">
      {{ error }}
    </div>
  {% endif %}
  <label for="inputName" class="sr-only">ユーザー名</label>
  <input type="text" id="inputEmail" class="form-control" name="username" placeholder="UserName" required autofocus>
  <label for="inputPassword" class="sr-only">パスワード</label>
  <input type="password" id="inputPassword" class="form-control"  name="password1" placeholder="Password" required>
  <label for="inputPassword" class="sr-only">パスワード(確認用)</label>
  <input type="password" id="inputPassword" class="form-control"  name="password2" placeholder="Password" required>
  <button class="btn btn-lg btn-primary btn-block" type="submit">サインアップ</button>
</form>
{% endblock %}


success_sign_up.py

アカウント作成が成功した場合に表示される。

tempates/registration/success_sign_up.html

{% extends "tyogokin/base.html" %}

{% block paging %}
{% endblock %}

{% block css %}
create_css
{% endblock %}

{% block main %}
<div class="center">
  <div class="alert alert-success" role="alert">
    アカウントの作成に成功
  </div>

  <a class="btn btn-primary" href="/tyogokin/login/" role="button"></a>
</div>
{% endblock %}


staticに自分で定義したCSS

bootstrapだけで実装できなかった細かな調整をするために記述したのもの

main.css

要素の細かな設定をしている。要素の場所など(中央配置)などをしている。

main/static/main.css

/* パソコンで見たときは"pc"のclassがついた画像が表示される */
.pc { display: block !important; }
.sp { display: none !important; }

/* スマートフォンで見たときは"sp"のclassがついた画像が表示される */
@media only screen and (max-width: 750px) {
    .pc { display: none !important; }
    .sp { display: block !important; }
}

.form-signin {
  width: 100%;
  max-width: 330px;
  padding: 15px;
  margin: auto;
}
.form-signin .checkbox {
  font-weight: 400;
}
.form-signin .form-control {
  position: relative;
  box-sizing: border-box;
  height: auto;
  padding: 10px;
  font-size: 16px;
}
.form-signin .form-control:focus {
  z-index: 2;
}
.form-signin input[type="email"] {
  margin-bottom: -1px;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
  margin-bottom: 10px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

.not_show{
display:none;
}


main.css

上記のものと同じ。Djangoアプリのstaticフォルダにあるか、プロジェクトのstaticフォルダにあるかの違いである。

static/main.css
/* パソコンで見たときは"pc"のclassがついた画像が表示される */
.pc { display: block !important; }
.sp { display: none !important; }

/* スマートフォンで見たときは"sp"のclassがついた画像が表示される */
@media only screen and (max-width: 750px) {
    .pc { display: none !important; }
    .sp { display: block !important; }
}

.form-signin {
  width: 100%;
  max-width: 330px;
  padding: 15px;
  margin: auto;
}
.form-signin .checkbox {
  font-weight: 400;
}
.form-signin .form-control {
  position: relative;
  box-sizing: border-box;
  height: auto;
  padding: 10px;
  font-size: 16px;
}
.form-signin .form-control:focus {
  z-index: 2;
}
.form-signin input[type="email"] {
  margin-bottom: -1px;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
  margin-bottom: 10px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

.not_show{
display:none;
}


少し変更したmanage.py

何回も実行し直すので億劫だった。ので、6行目に必要な情報を先に定義して、実行するだけにした。

manage.py

#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys

list = ['manage.py', 'runserver', '192.168.40.139:8000']

def main():
    """Run administrative tasks."""
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cloud_storage.settings")
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    #execute_from_command_line(sys.argv)
    execute_from_command_line(list)


if __name__ == "__main__":
    main()

複数のファイルを同時にアップロード

1つ1つアップロードすると非常に苦労するので、まとめてファイルをアップロードできるようにした。

views.py
def post(self, request):
        if request.method == 'POST':
            form = UploadForm(request.POST, request.FILES)
            if form.is_valid():
                for ff in request.FILES.getlist('file_name'):
                    re_user_name = request.user.username
                    p = Post(file_name=ff,user_name=re_user_name)
                    p.save()

上記のPythonコードは、views.pyのCREATEクラスの一部を抜粋したものである。

post_form.html
<input class="my-4 form-control form-control-lg" multiple id="formFileLg" type="file" name="file_name" value="" required>

上記のhtmlコードは、post_form.htmlの一部を抜粋したものである。multipleを指定する必要がある。

まとめ

安価で実装でき、かつ必要な知識を学ぶことができた。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1